很多時候,當我們跟著原始碼去理解某種事物時,基本上可以認為是以時間順序展開,這是編年體的邏輯。還有另一種邏輯,紀傳體,它以人物為中心編排史事,使得讀者更聚焦於某個人物。以一種新的視角,把所有的事情串連起來,令人大呼過癮。今天我們試著以這樣一種邏輯再看 g0。
回顧一下 Go 夜讀第 78 期,關於排程器原始碼分析的內容。我們講過,與主執行緒繫結的 M 對應的 g0 的主要作用是提供一個比一般 goroutine 要大的多棧(64K)供 runtime 程式碼執行。
初始化的過程中,在函式 runtime·rt0_go
裡會給主執行緒的 g0 分配棧空間:
之後,主執行緒會與 m0 繫結,m0 又與 g0 繫結:
之後,又與 p0 繫結:
這樣,主執行緒的這一套 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 裡為 1
g0
作為一個特殊的 goroutine,為 scheduler 執行排程迴圈提供了場地(棧)。對於一個執行緒來說,g0 總是它第一個建立的 goroutine。之後,它會不斷地尋找其他普通的 goroutine 來執行,直到程式退出。
當需要執行一些任務,且不想擴棧時,就可以用到 g0 了,因為 g0 的棧比較大。g0 其他的一些“職責”有:建立 goroutine、deferproc 函式裡新建 _defer、垃圾回收相關的工作(例如 stw、掃描 goroutine 的執行棧、一些標識清掃的工作、棧增長)等等。
因為 g0 這樣一個特殊的 goroutine 所做的工作,使得 Go 程式執行地更快。
注:最近在 medium 上看到了一個非常讚的關於 Go 的部落格,題圖畫得很有閱讀的慾望。這篇文章也是參考於其中的一篇。