1. 程式人生 > 其它 >goroutine 返回值_Goroutine 洩露:被遺忘的傳送者

goroutine 返回值_Goroutine 洩露:被遺忘的傳送者

技術標籤:goroutine 返回值

(給Go開發大全加星標)

英文:Jacob Walker,翻譯:wumansgy

https://www.ardanlabs.com/blog/2018/11/goroutine-leaks-the-forgotten-sender.html

【導讀】goroutines 啟動非常簡單,不過無限制地不小心地使用goroutine會導致很多問題。本文針對gouroutine的洩露進行了舉例和分析。

引言

併發程式設計允許開發人員使用多個執行路徑解決問題,並且通常用於提高效能。併發並不意味著這些多路徑是並行執行的;它意味著這些路徑是無序執行的而不是順序執行。從歷史上看,使用由標準庫或第三方開發人員提供的庫可以促進這種型別的程式設計。

在 Go 中,語言本身和程式執行時內建了 Goroutines 和 channel 等併發特性,以減少或消除對庫的需求。這很容易在 Go 中編寫併發程式時造成錯覺。在你決定使用併發時必須要謹慎,因為如果沒有正確使用它那麼就會帶來一些稀罕的副作用或陷阱。如果你不小心,這些陷阱會產生複雜的問題和令人討厭的 bug。

我在這篇文章中討論的陷阱會與 Goroutine 洩漏有關。

Goroutines 洩露

一種常見的記憶體洩漏型別就是 Goroutines 洩漏。如果你開始了一個你認為最終會終止但是它永遠不會終止的 Goroutine,那麼它就會洩露了。它的生命週期為程式的生命週期,任何分配給 Goroutine 的記憶體都不能釋放。所以在這裡建議“永遠不要在不知道如何停止的情況下,就去開啟一個 Goroutine ”。

要弄明白基本的 Goroutine 洩漏,請檢視以下程式碼:

清單 1

// leak 是一個有 bug 程式。它啟動了一個 goroutine// 阻塞接收 channel。一切都將不復存在// 向那個 channel 傳送資料,並且那個 channel 永遠不會關閉// 那個 goroutine 會被永遠鎖死func leak() {     ch := make(chan int)     go func() {        val :=         fmt.Println("We received a value:", val)    }()}

清單 1 中定義了一個名為 leak 的函式。該函式在第 6 行建立一個 channel,該 channel 允許 Goroutines 傳遞整型資料。然後在第 8 行建立 Goroutine,它在第 9 行被阻塞,等待從 channel 中接收資料。當 Goroutine 正在等待時,leak 函式會結束返回。此時,程式的其他任何部分都不能通過 channel 傳送資料。這使得 Goroutine 在第 9 行被無限期的等待。第 10 行的 fmt.Println 呼叫永遠不會發生。

在本例中,Goroutine 洩漏可以在程式碼檢查期間快速識別。不幸的是,生產程式碼中的 Goroutine 洩漏通常更難找到。我無法展示 Goroutine 洩漏可能發生的所有方式,但是這篇文章將詳細說明你可能遇到的某種 Goroutine 洩漏。

洩露:被遺忘的傳送者

對於這個洩漏示例,你將看到一個無限期阻塞的 Goroutine,等待在通道上傳送一個值。

我們要看的程式會根據一些搜尋詞找到一個記錄,然後打印出來。這個程式是圍繞一個叫做 search 的函式構建的 :

清單 2

// search 模擬成一個查詢記錄的函式// 在查詢記錄時。執行此工作需要 200 ms。func search(term string) (string, error) {     time.Sleep(200 * time.Millisecond)     return "some value", nil}

清單 2 中第 3 行的 search 函式是一個模擬實現,用於模擬長時間執行的操作,如資料庫查詢或 Web 呼叫。在這個例子中,硬編碼需要 200 ms。

在清單 3 中程式呼叫 search 函式,如下:

清單 3

// process 函式是在該程式中搜索一條記錄// 然後列印它func process(term string) error {    record, err := search(term)    if err != nil {        return err    }   fmt.Println("Received:", record)   return nil}

在清單 3 中的第 3 行,定義了一個名為 process 的函式,它接受一個表示搜尋項的字串引數。在第 4 行,term 變數傳遞給 serach 函式,該函式返回查詢到的記錄和錯誤。如果發生錯誤,則將錯誤返回到第 6 行的呼叫方。如果沒有錯誤,則在第 9 行列印該記錄。

對於某些應用程式來說,順序呼叫 search 函式時產生的延遲可能是無法接受的。假設不能使 search 函式執行得更快,則可以將 process 函式更改為不消耗 search 所產生的總延遲成本。

為此,我們可以像下面清單 4 中那種使用 Goroutine,不幸的是,這第一次嘗試是錯誤的,因為它造成了潛在的 Goroutine 洩漏。

清單 4

// serach 函式得到的返回值用 result 結構體來儲存// 通過單個 channel 來傳遞這兩個值type result struct {    record string    err    error}// process 函式是一個用來尋找記錄的函式// 然後列印,如果超過 100 ms 就會失敗 .func process(term string) error {     // 建立一個在 100 ms 內取消上下文的 context     ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)     defer cancel()     // 為 Goroutine 建立一個傳遞結果的 channel     ch := make(chan result)     // 啟動一個 goroutine 來尋找記錄,然後得到結果     // 將返回值從 channel 中返回     go func() {         record, err := search(term)         ch      }()     // 阻塞等待從 goroutine 接收值     // 通過 channel 和 context 來取消上下文操作     select {     case          return errors.New("search canceled")     case result :=          if result.err != nil {            return result.err         }         fmt.Println("Received:", result.record)         return nil    } }

在清單 4 中的第 13 行,重寫 process 函式以建立 Context 來在 100 ms 內取消上下文。有關如何使用 Context 的更多資訊,請閱讀 go 語言開發文件。

然後在第 17 行,程式建立一個無緩衝的 channel,允許 Goroutines 傳遞 result 型別的資料。在第 21 到 24 行,定義了匿名函式,此處稱為 Goroutine。此 Goroutine 呼叫 search 函式並嘗試通過第 23 行的 channel 傳送其返回值。

當 Goroutine 正在執行其工作時,process 函式執行第 28 行上的 select 模組。該模組有兩種情況,它們都是 channel 接收操作。

在第 29 行,有一個從 ctx.Done() channel 接收的 case。如果上下文被取消(100 ms 持續時間到達),將執行此 case。如果執行此 case,則 process 函式將返回錯誤,代表著取消了等待第 30 行的 search。

或者,第 31 行上的 case 從 ch channel 接收並將值分配給名為 result 的變數。與前面在順序實現中一樣,程式在第 32 行和第 33 行檢查和處理錯誤。如果沒有錯誤,程式將在第 35 行列印記錄,並返回 nil 以指示成功。

此重構設定了 process 函式等待 search 完成的最大持續時間。然而,這種實現也會造成潛在的 Goroutine 洩漏。想想程式碼中的 Goroutine 在做什麼;在第 23 行,它通過 channel 傳送。在此 channel 上傳送將阻塞執行,直到另一個 Goroutine 準備接收值為止。在超時的情況下,接收方停止等待 Goroutine 的接收並繼續工作。這將導致 Goroutine 永遠阻塞,等待一個永遠不會發生的接收器出現。這就是 Goroutine 洩露的時候。

修復:創造一些空間

解決此洩漏的最簡單方法是將無緩衝 channel 更改為容量為 1 的緩衝通道。

清單 5

// 為 Goroutine 建立一個傳遞結果的 channel。// 給它容量,以至於傳送接受不會阻塞。   ch := make(chan result, 1)

現在在超時情況下,在接收器繼續執行之後,搜尋 Goroutine 將通過將結果值放入 channel 來完成其傳送,然後它將返回。Goroutine 的記憶體以及 channel 的記憶體最終將會被收回。一切都會自然而然地發揮作用。

在 channel 的行為中,William Kennedy 提供了幾個關於 channel 行為的很好的例子,並提供了有關其使用的哲學。該文章“清單 10”的最後一個示例顯示了一個類似於此超時示例的程式。閱讀該文章,獲取有關何時使用緩衝 channel 以及適當的容量級別的更多建議。

結論

Go 讓啟動 goroutines 變得簡單,但我們有責任明智地使用它們。在這篇文章中,我展示瞭如何錯誤地使用 Goroutines 的一個例子。有許多方法可以建立 goroutine 洩漏以及使用併發時可能遇到的其他陷阱。併發是一個有用的工具,但必須謹慎使用。

- EOF -

推薦閱讀(點選標題可開啟)

1、如何優雅關閉後臺goroutine?

2、基於 etcd 實現分散式鎖

3、開源goraft原始碼分析

如果覺得本文不錯,歡迎轉發推薦給更多人。

db19fa4334dc67a5ad4734a13ace2653.png

分享、點贊和在看

支援我們分享更多好文章,謝謝!