Go語言Goroutines?泄漏場景與防治解決分析
場景
Go 有很多自動(dòng)管理內(nèi)存的功能。比如:
- 變量分配到堆內(nèi)存還是棧內(nèi)存,編譯器會(huì)通過逃逸分析(escpage analysis)來判斷;
- 堆內(nèi)存的垃圾自動(dòng)回收。
即便如此,如果編碼不謹(jǐn)慎,我們還是有可能導(dǎo)致內(nèi)存泄漏的,最常見的是 goroutine 泄漏,比如下面的函數(shù):
func goroutinueLeak() { ch := make(chan int) go func(ch chan int) { // 因?yàn)?ch 一直沒有數(shù)據(jù),所以這個(gè)協(xié)程會(huì)阻塞在這里。 val := <-ch fmt.Println(val) }(ch) }
由于ch
一直沒有發(fā)送數(shù)據(jù),所以我們開啟的 goroutine 會(huì)一直阻塞。每次調(diào)用goroutinueLeak
都會(huì)泄漏一個(gè)goroutine,從監(jiān)控面板看到話,goroutinue 數(shù)量會(huì)逐步上升,直至服務(wù) OOM。
Goroutine 泄漏常見原因
channel 發(fā)送端導(dǎo)致阻塞
使用 context 設(shè)置超時(shí)是常見的一個(gè)場景,試想一下,下面的函數(shù)什么情況下會(huì) goroutine 泄漏 ?
func contextLeak() error { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() ch := make(chan int) //g1 go func() { // 獲取數(shù)據(jù),比如網(wǎng)絡(luò)請求,可能時(shí)間很久 val := RetriveData() ch <- val }() select { case <-ctx.Done(): return errors.New("timeout") case val := <-ch: fmt.Println(val) } return nil }
RetriveData()
如果超時(shí)了,那么contextLeak()
會(huì)返回 error,本函數(shù)執(zhí)行結(jié)束。而我們開啟的協(xié)程g1
,由于沒有接受者,會(huì)阻塞在 ch<-val
。
解決方法也能簡單,比如可以給ch
加上緩存。
channel 接收端導(dǎo)致阻塞
開篇給出的函數(shù)goroutinueLeak
,就是因?yàn)閏hannel的接收端收不到數(shù)據(jù),導(dǎo)致阻塞。
這里舉出另一個(gè)例子,下面的函數(shù),是否有可能 goroutinue 泄漏?
func errorAssertionLeak() { ch := make(chan int) // g1 go func() { val := <-ch fmt.Println(val) }() // RetriveSomeData 表示獲取數(shù)據(jù),比如從網(wǎng)絡(luò)上 val, err := RetriveSomeData() if err != nil { return } ch <- val return nil }
如果 RetriveSomeData()
返回的err
不為 nil
,那么本函數(shù)中斷,也就不會(huì)有數(shù)據(jù)發(fā)送給ch
,這導(dǎo)致協(xié)程g1
會(huì)一直阻塞。
如何預(yù)防
goroutine 泄漏往往需要服務(wù)運(yùn)行一段時(shí)間后,才會(huì)被發(fā)覺。
我們可以通過監(jiān)控 goroutine 數(shù)量來判斷是否有 goroutine 泄漏;或者用 pprof(之前文章介紹過的) 來定位泄漏的 goroutine。但這些已經(jīng)是亡羊補(bǔ)牢了。最理想的情況是,我們在開發(fā)的過程中,就能發(fā)現(xiàn)。
本文推薦的做法是,使用單元測試。以開篇的 goroutinueLeak
為例子,我們寫個(gè)單測:
func TestLeak(t *testing.T) { goroutinueLeak() }
執(zhí)行 go test,發(fā)現(xiàn)測試是通過的:
=== RUN TestLeak
--- PASS: TestLeak (0.00s)
PASS
ok example/leak 0.598s
這是是因?yàn)閱螠y默認(rèn)不會(huì)檢測 goroutine 泄漏的。
我們可以在單測中,加入U(xiǎn)ber 團(tuán)隊(duì)提供的 uber-go/goleak
包:
import ( "testing" "go.uber.org/goleak" ) func TestLeak(t *testing.T) { // 加上這行代碼,就會(huì)自動(dòng)檢測是否 goroutine 泄漏 defer goleak.VerifyNone(t) goroutinueLeak() }
這時(shí)候執(zhí)行 go test,輸出:
=== RUN TestLeak
/xxx/leak_test.go:12: found unexpected goroutines:
[Goroutine 21 in state chan receive, with example/leak.goroutinueLeak.func1 on top of the stack:
goroutine 21 [chan receive]:
example/leak.goroutinueLeak.func1(0x0)
/xxx/leak.go:9 +0x27
created by example/leak.goroutinueLeak
/xxx/leak.go:8 +0x7a
]
--- FAIL: TestLeak (0.46s)
FAIL
FAIL example/leak 0.784s
這時(shí)候單測會(huì)因?yàn)?goroutine 泄漏而不通過。
如果你覺得每個(gè)測試用例都要加上 defer goleak.VerifyNone(t)
太繁瑣的話(特別是在已有的項(xiàng)目中加上),goleak 提供了在 TestMain 中使用的方法VerifyTestMain
,上面的單測可以修改成:
func TestLeak(t *testing.T) { goroutinueLeak() } func TestMain(m *testing.M) { goleak.VerifyTestMain(m) }
總結(jié)
雖然我的文章經(jīng)常提及單測,但我本人不是單元測試的忠實(shí)粉絲。扎實(shí)的基礎(chǔ),充分的測試,負(fù)責(zé)任的態(tài)度也是非常重要的。
引用
以上就是Go語言Goroutines 泄漏場景與防治解決分析的詳細(xì)內(nèi)容,更多關(guān)于Go Goroutines 泄漏防治的資料請關(guān)注腳本之家其它相關(guān)文章!
- GoRoutines高性能同時(shí)進(jìn)行多個(gè)Api調(diào)用實(shí)現(xiàn)
- 盤點(diǎn)總結(jié)2023年Go并發(fā)庫有哪些變化
- Go語言單線程運(yùn)行也會(huì)有的并發(fā)問題解析
- Go并發(fā)原語之SingleFlight請求合并方法實(shí)例
- go并發(fā)數(shù)據(jù)一致性事務(wù)的保障面試應(yīng)答
- Go并發(fā)編程結(jié)構(gòu)體多字段原子操作示例詳解
- Go語言動(dòng)態(tài)并發(fā)控制sync.WaitGroup的靈活運(yùn)用示例詳解
- Go中Goroutines輕量級(jí)并發(fā)的特性及效率探究
相關(guān)文章
golang動(dòng)態(tài)創(chuàng)建類的示例代碼
這篇文章主要介紹了golang動(dòng)態(tài)創(chuàng)建類的實(shí)例代碼,本文通過實(shí)例代碼給大家講解的非常詳細(xì),需要的朋友可以參考下2023-06-06golang中package?is?not?in?GOROOT報(bào)錯(cuò)的真正解決辦法
這篇文章主要給大家介紹了關(guān)于golang中package?is?not?in?GOROOT報(bào)錯(cuò)的真正解決辦法,文中通過圖文介紹的非常詳細(xì),對同樣遇到這個(gè)問題的朋友具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-03-03Go語言中實(shí)現(xiàn)完美錯(cuò)誤處理實(shí)踐分享
Go?語言是一門非常流行的編程語言,由于其高效的并發(fā)編程和出色的網(wǎng)絡(luò)編程能力,越來越受到廣大開發(fā)者的青睞。本文我們就來深入探討一下Go?語言中的錯(cuò)誤處理機(jī)制吧2023-04-04