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