Goroutine 一泄露就看到它,這個是什么?
大家好,我是煎魚。
作為一個 Go 語言的使用大戶,常常就有人冷不丁的,一下就泄露了...泄露了啥。煎魚幫我看看?
表象來看當然是 goroutine 泄露了,這時候就會有小伙伴開始跑去拉取 PProf。就會看到類似下面這張圖:

重點會看到 runtime.gopark 這個函數(shù),在所有的 goroutine 泄露中都會看到有,并且都會是大頭。
既然是大頭,也就會有許多朋友以為他是泄漏點,在那一頓猛查,那這個函數(shù)到底是什么,作用是?
runtime.gopark 是何物
想要知道 runtime.gopark 函數(shù)是作用,最快的辦法就是看源碼了。其實現(xiàn)細節(jié)在 src/runtime/proc.go 文件中。
源代碼如下:
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
mp := acquirem()
gp := mp.curg
status := readgstatus(gp)
mp.waitlock = lock
mp.waitunlockf = unlockf
gp.waitreason = reason
mp.waittraceev = traceEv
mp.waittraceskip = traceskip
releasem(mp)
mcall(park_m)
}
該函數(shù)主要作用有三大點:
調(diào)用 acquirem函數(shù):獲取當前 goroutine 所綁定的 m,設(shè)置各類所需數(shù)據(jù)。 調(diào)用 releasem函數(shù)將當前 goroutine 和其 m 的綁定關(guān)系解除。調(diào)用 park_m函數(shù):將當前 goroutine 的狀態(tài)從 _Grunning切換為_Gwaiting,也就是等待狀態(tài)。刪除 m 和當前 goroutine m->curg(簡稱gp)之間的關(guān)聯(lián)。 調(diào)用 mcall函數(shù),僅會在需要進行 goroutiine 切換時會被調(diào)用:切換當前線程的堆棧,從 g 的堆棧切換到 g0 的堆棧并調(diào)用 fn(g) 函數(shù)。 將 g 的當前 PC/SP 保存在 g->sched 中,以便后續(xù)調(diào)用 goready 函數(shù)時可以恢復(fù)運行現(xiàn)場。
熟讀了其源碼后,我們可得知該函數(shù)的關(guān)鍵作用就是將當前的 goroutine 放入等待狀態(tài),這意味著 goroutine 被暫時被擱置了,也就是被運行時調(diào)度器暫停了。
緣由
回到最初的問題,之所以 goroutine 泄露,你就會看到大量的 runtime.gopark 函數(shù),這是因為 goroutine 泄露一般不會單單只是一個 goroutine,肯定是會有多個的。
同時這些 goroutine 在調(diào)用了 runtime.gopark 函數(shù)后都被暫停了,也就是進入休眠狀態(tài),自然而然也就停留在此。
直至滿足條件后再被 runtime.goready 函數(shù)喚醒,該函數(shù)會將已準備就緒的 goroutine 切換狀態(tài),再加入運行隊列,等待調(diào)度器的新一輪調(diào)度。
思考
前幾天就有讀者在我的 Go 讀者群(可以加我后拉你進群)中咨詢了下述問題,也和 runtime.gopark 函數(shù)有關(guān)。問題如下:

經(jīng)過上述的分析,顯然 runtime.gopark 不是 goroutine 的一種狀態(tài),導(dǎo)致 goroutine 狀態(tài)變更只是他的執(zhí)行過程中所涉及到,產(chǎn)生的一個結(jié)果。
而 goroutine 的狀態(tài)一共有 9 種,有興趣的小伙伴可以了解。如下:
| 狀態(tài) | 含義 |
|---|---|
| _Gidle | 剛剛被分配,還沒有進行初始化。 |
| _Grunnable | 已經(jīng)在運行隊列中,還沒有執(zhí)行用戶代碼。 |
| _Grunning | 不在運行隊列里中,已經(jīng)可以執(zhí)行用戶代碼,此時已經(jīng)分配了 M 和 P。 |
| _Gsyscall | 正在執(zhí)行系統(tǒng)調(diào)用,此時分配了 M。 |
| _Gwaiting | 在運行時被阻止,沒有執(zhí)行用戶代碼,也不在運行隊列中,此時它正在某處阻塞等待中。 |
| _Gmoribund_unused | 尚未使用,但是在 gdb 中進行了硬編碼。 |
| _Gdead | 尚未使用,這個狀態(tài)可能是剛退出或是剛被初始化,此時它并沒有執(zhí)行用戶代碼,有可能有也有可能沒有分配堆棧。 |
| _Genqueue_unused | 尚未使用。 |
| _Gcopystack | 正在復(fù)制堆棧,并沒有執(zhí)行用戶代碼,也不在運行隊列中。 |
總結(jié)
在今天這篇文章中,我們介紹了大家最常碰到的 goroutine 泄露,而在泄露后最關(guān)心的 runtime.gopark 函數(shù)的意義,我們從源碼再到作用進行了一輪剖析。
下次如果再有人問你 runtime.gopark 是干嘛用的,就可以愉快的把這篇文章甩給他,分享你的知識啦 :)
關(guān)注煎魚,吸取他的知識 ??

你好,我是煎魚。高一折騰過前端,參加過國賽拿了獎,大學(xué)搞過 PHP?,F(xiàn)在整 Go,在公司負責(zé)微服務(wù)架構(gòu)等相關(guān)工作推進和研發(fā)。
從大學(xué)開始靠自己賺生活費和學(xué)費,到出版 Go 暢銷書《Go 語言編程之旅》,再到獲得 GOP(Go 領(lǐng)域最有觀點專家)榮譽,點擊藍字查看我的出書之路。
日常分享高質(zhì)量文章,輸出 Go 面試、工作經(jīng)驗、架構(gòu)設(shè)計,加微信拉讀者交流群,記得點贊!
