聊聊 g0
很多時候,當我們跟著源碼去理解某種事物時,基本上可以認為是以時間順序展開,這是編年體的邏輯。還有另一種邏輯,紀傳體,它以人物為中心編排史事,使得讀者更聚焦于某個人物。以一種新的視角,把所有的事情串連起來,令人大呼過癮。今天我們試著以這樣一種邏輯再看 g0。
回顧一下 Go 夜讀第 78 期,關(guān)于調(diào)度器源碼分析的內(nèi)容。我們講過,與主線程綁定的 M 對應的 g0 的主要作用是提供一個比一般 goroutine 要大的多棧(64K)供 runtime 代碼執(zhí)行。
初始化的過程中,在函數(shù) runtime·rt0_go 里會給主線程的 g0 分配??臻g:
g0 棧空間之后,主線程會與 m0 綁定,m0 又與 g0 綁定:
主線程綁定 m0,g0之后,又與 p0 綁定:
g0-p0-m0這樣,主線程的這一套 GPM 就可以轉(zhuǎn)起來了。接著,就創(chuàng)建了 main goroutine,放入 p0 的本地待運行隊列。最后,通過 schedule() 函數(shù)進入調(diào)度循環(huán)。
前面說的是程序初始化的過程中,g0 是如何誕生的。當執(zhí)行到 main.main() 函數(shù),也說是用戶在 main 包下寫的 main 函數(shù)里,我們隨手一句:
go func() {// 要做的事 }()就啟動了一個 goroutine 時,在 Go 編譯器的作用下,最終會轉(zhuǎn)化成 newproc 函數(shù)。在 newproc 函數(shù)的內(nèi)部,會在 g0 棧上調(diào)用 newproc1 函數(shù),完成后續(xù)的工作。創(chuàng)建完成后,會將新創(chuàng)建的 goroutine 放入 _p_ 的本地待運行隊列。
因為新增加了一個 g,這時會嘗試去喚醒一個 P 來一起執(zhí)行任務。判斷條件是:
if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {wakep() }即在有空閑 P 以及沒有正在“找工作的 M”的情況下,才會嘗試去喚醒一個 P。我們又知道,其實 P 的數(shù)量在程序運行過程中一般不會變化,所以這里所謂的喚醒其實就是把空閑的 P 利用起來。
通過 wakep() -> startm() -> newm() -> allocm() -> malg() 這條鏈路創(chuàng)建 g0,這里 g0 的棧大小實際上為 8KB。
mp.g0 = malg(8192 * sys.StackGuardMultiplier) // sys.StackGuardMultiplier 在 linux 里為 1g0 作為一個特殊的 goroutine,為 scheduler 執(zhí)行調(diào)度循環(huán)提供了場地(棧)。對于一個線程來說,g0 總是它第一個創(chuàng)建的 goroutine。之后,它會不斷地尋找其他普通的 goroutine 來執(zhí)行,直到進程退出。
當需要執(zhí)行一些任務,且不想擴棧時,就可以用到 g0 了,因為 g0 的棧比較大。g0 其他的一些“職責”有:創(chuàng)建 goroutine、deferproc 函數(shù)里新建 _defer、垃圾回收相關(guān)的工作(例如 stw、掃描 goroutine 的執(zhí)行棧、一些標識清掃的工作、棧增長)等等。
因為 g0 這樣一個特殊的 goroutine 所做的工作,使得 Go 程序運行地更快。
注:最近在 medium 上看到了一個非常贊的關(guān)于 Go 的博客博客[1],題圖畫得很有閱讀的欲望。這篇文章也是參考于其中的一篇[2]。
References
[1]?博客:?https://medium.com/a-journey-with-go
[2]?其中的一篇:?https://medium.com/a-journey-with-go/go-g0-special-goroutine-8c778c6704d8
總結(jié)
- 上一篇: defer 的前世今生
- 下一篇: 深度解密Go语言之sync.pool