您好,登錄后才能下訂單哦!
怎么在Golang中防止 goroutine 泄露?很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
package main import ( "fmt" "runtime" "time" ) func sayHello() { for { fmt.Println("Hello gorotine") time.Sleep(time.Second) } } func main() { defer func() { fmt.Println("the number of goroutines: ", runtime.NumGoroutine()) }() go sayHello() fmt.Println("Hello main") }
對 Go 比較熟悉的話,很容易發現這段代碼的問題,sayHello 是個死循環,沒有如何退出機制,因此也就沒有任何辦法釋放創建的 goroutine。我們通過在 main 函數最前面的 defer 實現在函數退出時打印當前運行中的 goroutine 數量,毫無意外,它的輸出如下:
the number of goroutines: 2
不過,因為上面的程序并非常駐,有泄露問題也不大,程序退出后系統會自動回收運行時資源。但如果這段代碼在常駐服務中執行,比如 http server,每接收到一個請求,便會啟動一次 sayHello,時間流逝,每次啟動的 goroutine 都得不到釋放,你的服務將會離奔潰越來越近。
這個例子比較簡單,我相信,對 Go 的并發稍微有點了解的朋友都不會犯這個錯。
泄露情況分類
前面介紹的例子由于在 goroutine 運行死循環導致的泄露。接下來,我會按照并發的數據同步方式對泄露的各種情況進行分析。簡單可歸于兩類,即:
channel 導致的泄露
傳統同步機制導致的泄露
傳統同步機制主要指面向共享內存的同步機制,比如排它鎖、共享鎖等。這兩種情況導致的泄露還是比較常見的。go 由于 defer 的存在,第二類情況,一般情況下還是比較容易避免的。
chanel 引起的泄露
先說 channel,如果之前讀過官方的那篇并發的文章[1],翻譯版[2],你會發現 channel 的使用,一個不小心就泄露了。我們來具體總結下那些情況下可能導致。
發送不接收
我們知道,發送者一般都會配有相應的接收者。理想情況下,我們希望接收者總能接收完所有發送的數據,這樣就不會有任何問題。但現實是,一旦接收者發生異常退出,停止繼續接收上游數據,發送者就會被阻塞。這個情況在 前面說的文章[3] 中有非常細致的介紹。
示例代碼:
package main import "time" func gen(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out } func main() { defer func() { fmt.Println("the number of goroutines: ", runtime.NumGoroutine()) }() // Set up the pipeline. out := gen(2, 3) for n := range out { fmt.Println(n) // 2 time.Sleep(5 * time.Second) // done thing, 可能異常中斷接收 if true { // if err != nil break } } }
例子中,發送者通過 out chan 向下游發送數據,main 函數接收數據,接收者通常會依據接收到的數據做一些具體的處理,這里用 Sleep 代替。如果這期間發生異常,導致處理中斷,退出循環。gen 函數中啟動的 goroutine 并不會退出。
如何解決?
此處的主要問題在于,當接收者停止工作,發送者并不知道,還在傻傻地向下游發送數據。故而,我們需要一種機制去通知發送者。我直接說答案吧,就不循漸進了。Go 可以通過 channel 的關閉向所有的接收者發送廣播信息。
修改后的代碼:
package main import "time" func gen(done chan struct{}, nums ...int) <-chan int { out := make(chan int) go func() { defer close(out) for _, n := range nums { select { case out <- n: case <-done: return } } }() return out } func main() { defer func() { time.Sleep(time.Second) fmt.Println("the number of goroutines: ", runtime.NumGoroutine()) }() // Set up the pipeline. done := make(chan struct{}) defer close(done) out := gen(done, 2, 3) for n := range out { fmt.Println(n) // 2 time.Sleep(5 * time.Second) // done thing, 可能異常中斷接收 if true { // if err != nil break } } }
函數 gen 中通過 select 實現 2 個 channel 的同時處理。當異常發生時,將進入 <-done 分支,實現 goroutine 退出。這里為了演示效果,保證資源順利釋放,退出時等待了幾秒保證釋放完成。
執行后的輸出如下:
the number of goroutines: 1
現在只有主 goroutine 存在。
接收不發送
發送不接收會導致發送者阻塞,反之,接收不發送也會導致接收者阻塞。直接看示例代碼,如下:
package main func main() { defer func() { time.Sleep(time.Second) fmt.Println("the number of goroutines: ", runtime.NumGoroutine()) }() var ch chan struct{} go func() { ch <- struct{}{} }() }
運行結果顯示:
the number of goroutines: 2
當然,我們正常不會遇到這么傻的情況發生,現實工作中的案例更多可能是發送已完成,但是發送者并沒有關閉 channel,接收者自然也無法知道發送完畢,阻塞因此就發生了。
解決方案是什么?那當然就是,發送完成后一定要記得關閉 channel。
nil channel
向 nil channel 發送和接收數據都將會導致阻塞。這種情況可能在我們定義 channel 時忘記初始化的時候發生。
示例代碼:
func main() { defer func() { time.Sleep(time.Second) fmt.Println("the number of goroutines: ", runtime.NumGoroutine()) }() var ch chan int go func() { <-ch // ch<- }() }
兩種寫法:<-ch 和 ch<- 1,分別表示接收與發送,都將會導致阻塞。如果想實現阻塞,通過 nil channel 和 done channel 結合實現阻止 main 函數的退出,這或許是可以一試的方法。
func main() { defer func() { time.Sleep(time.Second) fmt.Println("the number of goroutines: ", runtime.NumGoroutine()) }() done := make(chan struct{}) var ch chan int go func() { defer close(done) }() select { case <-ch: case <-done: return } }
在 goroutine 執行完成,檢測到 done 關閉,main 函數退出。
真實的場景
真實的場景肯定不會像案例中的簡單,可能涉及多階段 goroutine 之間的協作,某個 goroutine 可能即使接收者又是發送者。但歸根到底,無論什么使用模式。都是把基礎知識組織在一起的合理運用。
傳統同步機制
雖然,一般推薦 Go 并發數據的傳遞,但有些場景下,顯然還是使用傳統同步機制更合適。Go 中提供傳統同步機制主要在 sync 和 atomic 兩個包。接下來,我主要介紹的是鎖和 WaitGroup 可能導致 goroutine 的泄露。
Mutex
和其他語言類似,Go 中存在兩種鎖,排它鎖和共享鎖,關于它們的使用就不作介紹了。我們以排它鎖為例進行分析。
示例如下:
func main() { total := 0 defer func() { time.Sleep(time.Second) fmt.Println("total: ", total) fmt.Println("the number of goroutines: ", runtime.NumGoroutine()) }() var mutex sync.Mutex for i := 0; i < 2; i++ { go func() { mutex.Lock() total += 1 }() } }
執行結果如下:
total: 1
the number of goroutines: 2
這段代碼通過啟動兩個 goroutine 對 total 進行加法操作,為防止出現數據競爭,對計算部分做了加鎖保護,但并沒有及時的解鎖,導致 i = 1 的 goroutine 一直阻塞等待 i = 0 的 goroutine 釋放鎖。可以看到,退出時有 2 個 goroutine 存在,出現了泄露,total 的值為 1。
怎么解決?因為 Go 有 defer 的存在,這個問題還是非常容易解決的,只要記得在 Lock 的時候,記住 defer Unlock 即可。
示例如下:
mutex.Lock() defer mutext.Unlock()
其他的鎖與這里其實都是類似的。
WaitGroup
WaitGroup 和鎖有所差別,它類似 Linux 中的信號量,可以實現一組 goroutine 操作的等待。使用的時候,如果設置了錯誤的任務數,也可能會導致阻塞,導致泄露發生。
一個例子,我們在開發一個后端接口時需要訪問多個數據表,由于數據間沒有依賴關系,我們可以并發訪問,示例如下:
package main import ( "fmt" "runtime" "sync" "time" ) func handle() { var wg sync.WaitGroup wg.Add(4) go func() { fmt.Println("訪問表1") wg.Done() }() go func() { fmt.Println("訪問表2") wg.Done() }() go func() { fmt.Println("訪問表3") wg.Done() }() wg.Wait() } func main() { defer func() { time.Sleep(time.Second) fmt.Println("the number of goroutines: ", runtime.NumGoroutine()) }() go handle() time.Sleep(time.Second) }
執行結果如下:
the number of goroutines: 2
出現了泄露。再看代碼,它的開始部分定義了類型為 sync.WaitGroup
的變量 wg,設置并發任務數為 4,但是從例子中可以看出只有 3 個并發任務。故最后的 wg.Wait()
等待退出條件將永遠無法滿足,handle 將會一直阻塞。
怎么防止這類情況發生?
我個人的建議是,盡量不要一次設置全部任務數,即使數量非常明確的情況。因為在開始多個并發任務之間或許也可能出現被阻斷的情況發生。最好是盡量在任務啟動時通過 wg.Add(1) 的方式增加。
示例如下:
... wg.Add(1) go func() { fmt.Println("訪問表1") wg.Done() }() wg.Add(1) go func() { fmt.Println("訪問表2") wg.Done() }() wg.Add(1) go func() { fmt.Println("訪問表3") wg.Done() }() ...
golang可以做服務器端開發,但golang很適合做日志處理、數據打包、虛擬機處理、數據庫代理等工作。在網絡編程方面,它還廣泛應用于web應用、API應用等領域。
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。