不少時候,當咱們跟着源碼去理解某種事物時,基本上能夠認爲是以時間順序展開,這是編年體的邏輯。還有另外一種邏輯,紀傳體,它以人物爲中心編排史事,使得讀者更聚焦於某我的物。以一種新的視角,把全部的事情串連起來,使人大呼過癮。今天咱們試着以這樣一種邏輯再看 g0。linux
回顧一下 Go 夜讀第 78 期,關於調度器源碼分析的內容。咱們講過,與主線程綁定的 M 對應的 g0 的主要做用是提供一個比通常 goroutine 要大的多棧(64K)供 runtime 代碼執行。golang
初始化的過程當中,在函數 runtime·rt0_go
裏會給主線程的 g0 分配棧空間:函數
以後,主線程會與 m0 綁定,m0 又與 g0 綁定:源碼分析
以後,又與 p0 綁定:atom
這樣,主線程的這一套 GPM 就能夠轉起來了。接着,就建立了 main goroutine,放入 p0 的本地待運行隊列。最後,經過 schedule()
函數進入調度循環。線程
前面說的是程序初始化的過程當中,g0 是如何誕生的。當執行到 main.main()
函數,也說是用戶在 main 包下寫的 main 函數裏,咱們隨手一句:code
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 利用起來。ip
經過 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 的博客,題圖畫得頗有閱讀的慾望。這篇文章也是參考於其中的一篇。