今天來簡單談談,Go 如何防止 goroutine 泄露。後端
Go 的併發模型與其餘語言不一樣,雖然說它簡化了併發程序的開發難度,但若是不瞭解使用方法,經常會遇到 goroutine 泄露的問題。雖然 goroutine 是輕量級的線程,佔用資源不多,但若是一直得不到釋放而且還在不斷建立新協程,毫無疑問是有問題的,而且是要在程序運行幾天,甚至更長的時間才能發現的問題。bash
對於上面描述的問題,我以爲能夠從兩方面入手解決,以下:微信
一是預防,要作到預防,咱們就須要瞭解什麼樣的代碼會產生泄露,以及瞭解如何寫出正確的代碼;併發
二是監控,雖然說預防減小了泄露產生的機率,但沒有人敢說本身不犯錯,於是,一般咱們還須要一些監控手段進一步保證程序的健壯性;函數
接下來,我將會分兩篇文章分別從這兩個角度進行介紹,今天先談第一點。post
本文主要集中在第一點上,但爲了更好的演示效果,能夠先介紹一個最簡單的監控方式。經過 runtime.NumGoroutine() 獲取當前運行中的 goroutine 數量,經過它確認是否發生泄漏。它的使用很是簡單,就不爲它專門寫個例子了。學習
語言級別的併發支持是 Go 的一大優點,但這個優點也很容易被濫用。一般咱們在開始 Go 併發學習時,經常聽別人說,Go 的併發很是簡單,在調用函數前加上 go 關鍵詞即可啓動 goroutine,即一個併發單元,但不少人可能只聽到了這句話,而後就出現了相似下面的代碼:ui
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 數量,毫無心外,它的輸出以下:atom
the number of goroutines: 2
複製代碼
不過,由於上面的程序並不是常駐,有泄露問題也不大,程序退出後系統會自動回收運行時資源。但若是這段代碼在常駐服務中執行,好比 http server,每接收到一個請求,便會啓動一次 sayHello,時間流逝,每次啓動的 goroutine 都得不到釋放,你的服務將會離奔潰愈來愈近。
這個例子比較簡單,我相信,對 Go 的併發稍微有點了解的朋友都不會犯這個錯。
前面介紹的例子因爲在 goroutine 運行死循環致使的泄露。接下來,我會按照併發的數據同步方式對泄露的各類狀況進行分析。簡單可歸於兩類,即:
傳統同步機制主要指面向共享內存的同步機制,好比排它鎖、共享鎖等。這兩種狀況致使的泄露仍是比較常見的。go 因爲 defer 的存在,第二類狀況,通常狀況下仍是比較容易避免的。
先說 channel,若是以前讀過官方的那篇併發的文章,翻譯版,你會發現 channel 的使用,一個不當心就泄露了。咱們來具體總結下那些狀況下可能致使。
咱們知道,發送者通常都會配有相應的接收者。理想狀況下,咱們但願接收者總能接收完全部發送的數據,這樣就不會有任何問題。但現實是,一旦接收者發生異常退出,中止繼續接收上游數據,發送者就會被阻塞。這個狀況在 前面說的文章 中有很是細緻的介紹。
示例代碼:
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 發送和接收數據都將會致使阻塞。這種狀況可能在咱們定義 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 的泄露。
和其餘語言相似,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 和鎖有所差異,它相似 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()
}()
...
複製代碼
大概介紹完了我認爲的全部可能致使 goroutine 泄露的狀況。總結下來,其實不管是死循環、channel 阻塞、鎖等待,只要是會形成阻塞的寫法均可能產生泄露。於是,如何防止 goroutine 泄露就變成了如何防止發生阻塞。爲進一步防止泄露,有些實現中會加入超時處理,主動釋放處理時間太長的 goroutine。
本篇主要從如何寫出正確代碼的角度來介紹如何防止 goroutine 的泄露。下篇,將會介紹如何實現更好的監控檢測,以幫助咱們發現當前代碼中已經存在的泄露。
Concurrency In Go
Goroutine leak
Leaking-Goroutines
Go Concurrency Patterns: Context
Go Concurrency Patterns: Pipelines and cancellation
make goroutine stay running after returning from function
Never start a goroutine without knowing how it will stop