一、Runtime
1. 為什么需要runtime
-
goroutines調(diào)度
goroutines是go的執(zhí)行單元,goroutines如果直接對應(yīng)操作系統(tǒng)的線程,go在調(diào)度goroutines時,勢必將像操作系統(tǒng)調(diào)度線程一樣,需要設(shè)置信號掩碼、CPU親和性及cgroup的資源管理等,這些額外的線程操作不是go運行g(shù)oroutines所需要的,這些操作將消耗大量資源、影響性能。所以go需要runtime來執(zhí)行g(shù)oroutines的調(diào)度,而不是讓操作系統(tǒng)調(diào)度線程。 -
垃圾回收
go是需要支持垃圾回收的,執(zhí)行垃圾回收時,我們需要保證goroutines處于暫停的狀態(tài),go的內(nèi)存才會處于一種一致的狀態(tài)。
當(dāng)沒有調(diào)度器時,線程是由操作系統(tǒng)來調(diào)度,但這對于go而言是不可控的,go無法很好的控制線程狀態(tài)及內(nèi)存。所以為了GC,go需要自己的調(diào)度器。goroutines有g(shù)o自身調(diào)度器控制才能確保內(nèi)存一致,才能正確地執(zhí)行GC。
所以要支持協(xié)程\線程調(diào)度就要有runtime。要支持垃圾回收就要有runtime。
2. 什么是runtime
上面可以分析出runtime所擔(dān)任的職責(zé):goroutines調(diào)度,垃圾回收,當(dāng)然還提供goroutines的執(zhí)行環(huán)境。
所以這也相當(dāng)于簡要解釋了什么是runtime。
go的可執(zhí)行程序可以分成兩個層:用戶代碼和運行時:
- 運行時提供接口函數(shù)供用戶代碼調(diào)用,用來管理goroutines,channels和其他一些內(nèi)置抽象結(jié)構(gòu)。
-
用戶代碼對操作系統(tǒng)API的任何調(diào)用都會被運行時層截取,以方便調(diào)度和垃圾回收。
二、GMP調(diào)度模型

- global queue(全局隊列):存放等待運行的G。為保證數(shù)據(jù)競爭問題,需要加鎖處理。
- local queue(本地隊列):本地隊列時無鎖的,可以可以提升處理速度。同全局隊列類似,存放的也是等待運行的G,存的數(shù)量有限,不超過256個。新建G'時,G'優(yōu)先加入到P的本地隊列,如果隊列滿了,則會把本地隊列中一半的G移動到全局隊列。
- P列表:所有的P都在程序啟動時創(chuàng)建,并保存在數(shù)組中,最多有GOMAXPROCS(可配置)個。在任何時候,每個P只能有一個M運行。
- M:線程想運行任務(wù)就得獲取P,從P的本地隊列獲取G,P隊列為空時,M也會嘗試從全局隊列拿一批G放到P的本地隊列,或從其他P的本地隊列偷一半放到自己P的本地隊列。M運行G,G執(zhí)行之后,M會從P獲取下一個G,不斷重復(fù)下去。
GMP主要包含4個基本單元:g、m、p、schedt。
- g是協(xié)程任務(wù)信息單元
- m是實際執(zhí)行體
- p是本地資源池和本地g任務(wù)池
- schedt是全局資源池和全局g任務(wù)池
下面我們詳細(xì)看下這4個基本單元的主要結(jié)構(gòu):
1. G
G是Goroutine的縮寫,是對Goroutine的抽象。其中包括執(zhí)行的函數(shù)指令及參數(shù);G保存著任務(wù)對象,線程上下文切換,現(xiàn)場保護(hù)和現(xiàn)場恢復(fù)需要的寄存器(SP、IP)等信息。
Goroutine的主要結(jié)構(gòu)(詳見runtime/runtime2.go)
type g struct {
stack stack // 描述了當(dāng)前 Goroutine 的棧內(nèi)存范圍 [stack.lo, stack.hi)
stackguard0 uintptr // 是對比 Go 棧增長的 prologue 的棧指針, 可以用于調(diào)度器搶占式調(diào)度
stackguard1 uintptr // 是對比 C 棧增長的 prologue 的棧指針
...
_panic *_panic // 最內(nèi)側(cè)的 panic 結(jié)構(gòu)體
_defer *_defer // 最內(nèi)側(cè)的延遲函數(shù)結(jié)構(gòu)體
m *m // 當(dāng)前的m
sched gobuf // goroutine切換時,用于保存g的上下文
...
param unsafe.Pointer // 用于傳遞參數(shù),睡眠時其他goroutine可以設(shè)置param,喚醒時該goroutine可以獲取
atomicstatus uint32 // Goroutine 的狀態(tài)
stackLock uint32
goid int64 // goroutine的ID
...
waitsince int64 // g被阻塞的大體時間
preempt bool // 搶占信號
preemptStop bool // 搶占時將狀態(tài)修改成 `_Gpreempted`
preemptShrink bool // 在同步安全點收縮棧
...
lockedm *m // G被鎖定只在這個m上運行
...
}
g中最主要的當(dāng)然是sched了,保存了goroutine的上下文。goroutine切換的時候,不同于線程有OS來負(fù)責(zé)這部分?jǐn)?shù)據(jù),而是由一個gobuf對象來保存,這樣能夠更加輕量級,再來看看gobuf的結(jié)構(gòu)(詳見runtime/runtime2.go):
type gobuf struct {
// The offsets of sp, pc, and g are known to (hard-coded in) libmach.
//
// ctxt is unusual with respect to GC: it may be a
// heap-allocated funcval, so GC needs to track it, but it
// needs to be set and cleared from assembly, where it's
// difficult to have write barriers. However, ctxt is really a
// saved, live register, and we only ever exchange it between
// the real register and the gobuf. Hence, we treat it as a
// root during stack scanning, which means assembly that saves
// and restores it doesn't need write barriers. It's still
// typed as a pointer so that any other writes from Go get
// write barriers.
sp uintptr // 棧指針
pc uintptr // 程序計數(shù)器
g guintptr // 當(dāng)前gobuf所屬的g
ctxt unsafe.Pointer
ret uintptr 系統(tǒng)調(diào)用的返回值
lr uintptr
bp uintptr // for framepointer-enabled architectures
}
2. M
M是一個線程或稱為Machine,M是有線程棧的。如果不對該線程棧提供內(nèi)存的話,系統(tǒng)會給該線程棧提供內(nèi)存(不同操作系統(tǒng)提供的線程棧大小不同)。當(dāng)指定了線程棧,則M.stack→G.stack,M的PC寄存器指向G提供的函數(shù),然后去執(zhí)行。
type m struct {
// g0是帶有調(diào)度棧的goroutine。
// 普通的Goroutine棧是在Heap分配的可增長的stack,而g0的stack是M對應(yīng)的線程棧。
// 所有調(diào)度相關(guān)代碼,會先切換到該Goroutine的棧再執(zhí)行。
g0 *g
......
gsignal *g // 處理信號的goroutine
......
tls [6]uintptr // thread-local storage
mstartfn func() //m入口函數(shù)
curg *g // 當(dāng)前運行的goroutine
caughtsig guintptr
p puintptr // 關(guān)聯(lián)p和執(zhí)行的go代碼
nextp puintptr
oldp puintptr // 在執(zhí)行系統(tǒng)調(diào)用之前所附加的p
id int32
mallocing int32 // 狀態(tài)
......
locks int32 //m的鎖
......
spinning bool // m不在執(zhí)行g(shù),但在積極尋找可執(zhí)行的g
blocked bool // m是否被阻塞
newSigstack bool
printlock int8
incgo bool // m是否在執(zhí)行cgo
freeWait uint32 // 如果為0,將安全釋放g0并刪除m(原子性)。
fastrand uint32
......
ncgocall uint64 // cgo調(diào)用的總數(shù)
ncgo int32 // 當(dāng)前cgo調(diào)用的數(shù)目
......
park note
alllink *m // 用于鏈接allm
schedlink muintptr
lockedg *g // 鎖定g在當(dāng)前m上執(zhí)行,而不會切換到其他m
createstack [32]uintptr // thread創(chuàng)建的棧
......
nextwaitm muintptr // 下一個等待的m
......
}
而g0的棧是M對應(yīng)的線程的棧。所有調(diào)度相關(guān)的代碼,會先切換到該goroutine的棧中再執(zhí)行。也就是說線程的棧也是用的g實現(xiàn),而不是使用的OS的。
3. P
P代表一個處理器,每一個運行的M都必須綁定一個P,就像線程必須在么一個CPU核上執(zhí)行一樣,由P來調(diào)度G在M上的運行,P的個數(shù)就是GOMAXPROCS(最大256),啟動時固定的,一般不修改;M的個數(shù)和P的個數(shù)不一定一樣多(會有休眠的M或者不需要太多的M)(最大10000);每一個P保存著本地G任務(wù)隊列,也有一個全局G任務(wù)隊列。P的數(shù)據(jù)結(jié)構(gòu):
type p struct {
id int32
status uint32 // 狀態(tài),可以為pidle/prunning/...
link puintptr
schedtick uint32 // 每調(diào)度一次加1
syscalltick uint32 // 每一次系統(tǒng)調(diào)用加1
sysmontick sysmontick
m muintptr // 回鏈到關(guān)聯(lián)的m
mcache *mcache //當(dāng)前m的內(nèi)存緩存,意味著不必為每一個M都配備一塊內(nèi)存,避免了過多的內(nèi)存消耗。
pcache pageCache
raceprocctx uintptr
......
// goroutine ids的緩存,攤銷對runtime-sched.goidgen的訪問。
goidcache uint64
goidcacheend uint64
// 可運行的goroutine的隊列. 不需要鎖即可訪問
runqhead uint32
runqtail uint32
runq [256]guintptr
runnext guintptr // 下一個運行的g,以高優(yōu)先級執(zhí)行 unblock G,提高了一些包的性能。
// 可用的G (status == Gdead, Gdead 表示這個goroutine目前未被使用)
gFree struct {
gList
n int32
}
// sudog 代表等待列表中的一個G,例如在向通道執(zhí)行發(fā)送/接收的G。
sudogcache []*sudog
sudogbuf [128]*sudog
// 堆中mspan對象的緩存
mspancache struct {
// len 被用于不允許寫障礙的調(diào)用代碼路徑中
len int
buf [128]*mspan
}
......
palloc persistentAlloc // per-P to avoid mutex
......
}
其中P的狀態(tài)有Pidle, Prunning, Psyscall, Pgcstop, Pdead;在其內(nèi)部隊列runqhead里面有可運行的goroutine,P優(yōu)先從內(nèi)部獲取執(zhí)行的g,這樣能夠提高效率。
4. schedt
除此之外,還有一個數(shù)據(jù)結(jié)構(gòu)需要在這里提及,就是schedt,可以看做是一個全局的調(diào)度者:
type schedt struct {
// 原子操作訪問
goidgen uint64
lastpoll uint64 // 最后一次網(wǎng)絡(luò)輪詢的時間,如果正在輪詢則為0
pollUntil uint64 // 當(dāng)前輪詢的睡眠時間
lock mutex
// When increasing nmidle, nmidlelocked, nmsys, or nmfreed, be
// sure to call checkdead().
midle muintptr // idle狀態(tài)的m
nmidle int32 // idle狀態(tài)的m個數(shù)
nmidlelocked int32 // lockde狀態(tài)的m個數(shù)
mnext int64 // 已經(jīng)創(chuàng)建的m的數(shù)量和下一個m的ID
maxmcount int32 // m允許的最大個數(shù)
nmsys int32 // 非locked狀態(tài)的系統(tǒng)M的數(shù)量
nmfreed int64 // 累計可用的M的數(shù)量
ngsys uint32 // 系統(tǒng)中g(shù)oroutine的數(shù)目,會自動更新
pidle puintptr // idle的p
npidle uint32
nmspinning uint32 // 尋找可執(zhí)行g(shù)otoutine的m數(shù)量
// goroutine全局可運行隊列.
runq gQueue
runqsize int32
......
// _Gdead狀態(tài)G的全局緩存
gFree struct {
lock mutex
stack gList // 有stack的Gs
noStack gList // 沒有stack的Gs
n int32
}
// sudog的Central 緩存
sudoglock mutex
sudogcache *sudog
......
// 等待被釋放的m的列表
freem *m
......
}
二、GMP調(diào)度細(xì)節(jié)分析
1. schedule
proc.go:3291 findrunnable()

- m是否阻塞?阻塞則sleep此m,其他m在從本地隊列尾部獲取g執(zhí)行,執(zhí)行完成后,繼續(xù)執(zhí)行schedule()
- 是否需要執(zhí)行g(shù)c?需要gc則停止spining、釋放p、開始sleep,gc完成后會被喚醒,繼續(xù)執(zhí)行schedule()
- 若存在gcBgMarkWorker則獲取此g,否則偶爾(
1/61概率)會從全局隊列獲取g(避免全局對列饑餓),沒有則從本地隊列獲取g。 - 若仍未獲取到g,則會進(jìn)入findrunnable流程,循環(huán)地去找g
- 獲取到g后,停止spining
- 獲取到鎖定的g,則將把對應(yīng)鎖定的m調(diào)度給當(dāng)前的p并喚醒m。否則該m將移交p給其他等待中的m并喚醒,該m將sleep
- 再次進(jìn)入schedule()循環(huán)
2. findrunnable
proc.go:2705 findrunnable():

- 是否需要執(zhí)行g(shù)c?需要gc則停止spining、釋放p、開始sleep,gc完成后會被喚醒,繼續(xù)執(zhí)行findrunnable()
- 從本地隊列尋找g,沒有則從全局隊列尋找g。獲取g后,將繼續(xù)schedule()
- 本地隊列、全局隊列都沒有g(shù),則查看是否存在net或file需要執(zhí)行。存在則獲取g,并繼續(xù)schedule()
- 沒有可執(zhí)行的g,開始spining,從其他p的本地隊列尾部竊取一半的g,并繼續(xù)scheudle()
- 其他p的本地隊列沒有g(shù),將會在sleep之前,再次嘗試查看全局隊列中是否有g(shù)
- 仍沒有g(shù),則釋放p,停止spining并準(zhǔn)備sleep
- 檢查是否有idle的p,則sleep,直到再次被喚醒
3. spining
線程自旋(spining)是相對于線程阻塞而言的,表象就是循環(huán)執(zhí)行一個指定邏輯(調(diào)度邏輯,目的是不停地尋找 G)
缺點: 始終獲取不到G時,自旋屬于空轉(zhuǎn),浪費CPU
優(yōu)點: 降低了 M 的上下文切換成本,提高了性能
GMP中有兩個地方會引入自旋:
- 類型1:沒有P的M找P掛載,保證一有 P 釋放就結(jié)合
- 類型2:沒有G的M找G運行,保證一有runnable的G就運行
由于P最多只有GOMAXPROCS,所以自旋的M最多只允許GOMAXPROCS個,多了就沒有意義了。
同時當(dāng)有類型1的自旋M存在時,類型2的自旋M就不阻塞,阻塞會釋放P,一釋放P就馬上被類型1的自旋M搶走了,沒必要。
有空閑的 P時,在以下3種場景,go調(diào)度器會確保至少有一個自旋 M 存在(喚醒或者創(chuàng)建一個 M):
-
新G創(chuàng)建之前
如果有空閑的 P,就意味著新 G 可以被立即執(zhí)行,即便不在同一個 P 也無妨,所以我們保留一個自旋的 M,就可以保證新 G 很快被運行。
為了執(zhí)行G,不需要關(guān)注在哪個P上運行,這時應(yīng)該不存在類型 1 的自旋只有類型 2 的自旋 -
M進(jìn)入系統(tǒng)調(diào)用(syscall)之前
當(dāng) M 進(jìn)入系統(tǒng)調(diào)用,意味著 M 不知道何時可以醒來,那么 M 對應(yīng)的 P 中剩下的 G 就得有新的 M 來執(zhí)行,所以我們保留一個自旋的 M 來執(zhí)行剩下的 G。
為了執(zhí)行P本地隊列中的G,需要和P綁定,這時應(yīng)該不存在類型 2 的自旋只有類型1的自旋 -
M從空閑變成活躍之前
如果M從空閑變成活躍,意味著可能一個處于自旋狀態(tài)的M進(jìn)入工作狀態(tài)了,這時要檢查并確保還有一個自旋M存在,以防還有G或者還有P空著的。
4. GMP模式優(yōu)點
- G分布在全局隊列和P本地隊列,全局隊列依舊是全局鎖,但是使用場景明顯很少;P 本地隊列使用無鎖隊列,使用原子操作來面對可能的并發(fā)場景。(解決了GM模式單一全局互斥鎖的問題)
- G 創(chuàng)建時就在 P 的本地隊列,可以避免在 P 之間傳遞(竊取除外),G 對 P 的數(shù)據(jù)局部性好; 當(dāng) G 開始執(zhí)行了,系統(tǒng)調(diào)用返回后 M 會嘗試獲取可用 P,獲取到了的話可以避免在 M 之間傳遞 。而且優(yōu)先獲取調(diào)用阻塞前的 P,所以 G 對 M 數(shù)據(jù)局部性好,G 對 P 的數(shù)據(jù)局部性也好。(解決了GM模式中G傳遞帶來的開銷問題,以及數(shù)據(jù)局部性問題)
- 內(nèi)存 mcache 只存在 P 結(jié)構(gòu)中,就不必為每一個M都配備一塊內(nèi)存了,避免過多的內(nèi)存消耗。P 最多只有 GOMAXPROCS 個,遠(yuǎn)小于 M 的個數(shù),所以也不會出現(xiàn)過多的內(nèi)存消耗。(解決了GM模式中內(nèi)存消耗大的問題)
- 通過引入自旋,保證任何時候都有處于等待狀態(tài)的自旋M,避免在等待可用的P和G時頻繁的阻塞和喚醒。(解決了GM模式中嚴(yán)重的線程阻塞/解鎖問題)
5. syscall阻塞情況下的調(diào)度
當(dāng)M1執(zhí)行某一個G時候如果發(fā)生了syscall或者其他阻塞操作后,M1會阻塞。如果當(dāng)前P中仍有一些G待執(zhí)行,runtime會將 Goroutine 的狀態(tài)更新至 _Gsyscall,將 Goroutine 的P和M暫時分離并更新P的狀態(tài)到 _Psyscall,表明這個 P 的 G 正在 syscall 中。
當(dāng)系統(tǒng)調(diào)用結(jié)束后,會調(diào)用退出系統(tǒng)調(diào)用的函數(shù) runtime.exitsyscall 為當(dāng)前 Goroutine 重新分配資源,該函數(shù)有兩個不同的執(zhí)行路徑:
- 調(diào)用
runtime.exitsyscallfast; - 切換至調(diào)度器的 Goroutine 并調(diào)用
runtime.exitsyscall0;
采用較快的路徑runtime.exitsyscallfast優(yōu)先來重新獲取原來的P,能獲取到就繼續(xù)綁回去,這樣有利于數(shù)據(jù)的局部性。runtime.exitsyscallfast中包含兩個不同的分支:
- 如果 Goroutine 的原P處于 _Psyscall 狀態(tài),會直接調(diào)用
wirep將 Goroutine 與原P進(jìn)行關(guān)聯(lián) - 如果原P不處于 _Psyscall 狀態(tài),且調(diào)度器中存在閑置的P,會調(diào)用 runtime.acquirep 使用閑置的P處理當(dāng)前 Goroutine;

如果通過runtime.exitsyscallfast獲取不到P,runtime就會采用另一個相對較慢的路徑 runtime.exitsyscall0 ,將當(dāng)前 Goroutine 切換至 _Grunnable 狀態(tài),并移除線程 M 和當(dāng)前 Goroutine 的關(guān)聯(lián),然后執(zhí)行以下邏輯分支:
- 當(dāng)我們通過
runtime.pidleget獲取到閑置的處理器時就會在該處理器上執(zhí)行 Goroutine; -
否則找不到空閑的P,runtime就會把 G 放回 global queue,M 放回到 idle list,等待調(diào)度器調(diào)度
6. sysmon搶占調(diào)度
sysmon 也叫監(jiān)控線程,它在一個單獨的 M 上執(zhí)行,無需 P 也可以運行,它是一個死循環(huán),每 20us~10ms 循環(huán)一次,循環(huán)完一次就 sleep 一會。為什么會是一個變動的周期呢,主要是避免空轉(zhuǎn),如果每次循環(huán)都沒什么需要做的事,那么 sleep 的時間就會加大。
1. sysmon的主要作用:
- 釋放閑置超過 5 分鐘的 span 物理內(nèi)存
- 如果超過 2 分鐘沒有垃圾回收,強(qiáng)制執(zhí)行
- 將長時間未處理的 netpoll 添加到全局隊列
- 向長時間運行的 G 任務(wù)發(fā)出
搶占調(diào)度 - 收回因 syscall 長時間阻塞的 P
2. 滿足什么條件會觸發(fā)搶占調(diào)度呢?
go 1.13 搶占調(diào)度
sysmon發(fā)現(xiàn)一個 P 一直處于running狀態(tài)超過了10ms,將調(diào)用preemptone 將 G 的stackguard0=stackPreempt,同時設(shè)置sched.gcwaiting=1。被標(biāo)記后,在該G調(diào)用新函數(shù)時,通過g.stackguard0判斷是否需要棧增長,需要棧增長就會通過morestack()檢查執(zhí)行schedule(),檢查到sched.gcwaiting==1時,就會讓當(dāng)前G讓出。
G設(shè)置了標(biāo)記位后,也不一定會被搶占。如果G調(diào)用的新函數(shù)是一個簡單的死循環(huán),將無法被搶占。如果G調(diào)用的新函數(shù)所需??臻g很少也不會被搶占,只有當(dāng)新函數(shù)觸發(fā)??臻g檢查(morestack()), 所需棧大于128字節(jié),才會被搶占。那么這里就帶來一些問題,我們看下下面的代碼示例:
package main
import "fmt"
func main(n int) {
go func(n int){
for{
n++
fmt.Println(n)
}
}(0)
for{}
}
在go 1.13版本之前,執(zhí)行上述代碼,會阻塞在go func()。原因是當(dāng)go的GC觸發(fā)時,會執(zhí)行STW。而STW會搶占所有的P,讓GC來運行。而go func()中是1個簡單的死循環(huán),這類操作無法進(jìn)行newstack、morestack、syscall,所以無法檢測stackguard0 == stackpreempt,也就不會執(zhí)行后續(xù)的schedule()讓當(dāng)前G讓出,導(dǎo)致阻塞。這種依賴棧增長的方式,不算是真正的搶占式調(diào)度。
go 1.14 搶占調(diào)度
在go 1.14版本實現(xiàn)了基于信號的搶占式調(diào)度。
- sysmon發(fā)現(xiàn)一個 P 一直處于running狀態(tài)超過了10ms,調(diào)用preemptone()方法時,會通過系統(tǒng)調(diào)用,向m發(fā)送sigPreempt信號。
- m收到信號后,會將信號交給sighandler處理
- sighandler確定信號為sigPreempt以后,調(diào)用doSigPreempt函數(shù)
- doSigPreempt函數(shù)在確認(rèn)P和G允許搶占,并可以安全地執(zhí)行搶占后,會向G的執(zhí)行上下文中注入異步搶占函數(shù)asyncPreempt。
- asyncPreempt匯編函數(shù)調(diào)用后,就會保存G的上下文,并調(diào)用schedule()讓當(dāng)前G讓出。
我們可以看到基于信號的搶占式調(diào)度,不再依賴于棧增長,即使空的for{}沒有執(zhí)行棧增長檢測代碼,也依然沒有阻塞,可以成功實現(xiàn)搶占式調(diào)度。
7. netpoller
1. 什么是netpoller
在Go的實現(xiàn)中,期望在用戶層面(程序員層面)所有IO都是阻塞調(diào)用的,Go的設(shè)計思想是程序員使用阻塞式的接口來編寫程序,然后通過goroutine+channel來處理并發(fā)。因此所有的IO邏輯都是直來直去的,先xx,再xx, 你不再需要回調(diào),不再需要future,要的僅僅是step by step。這對于代碼的可讀性是很有幫助的。
但是如果在Runtime內(nèi)部也采用阻塞 I/O 調(diào)用,那么物理線程將也處于阻塞狀態(tài),導(dǎo)致大量資源的浪費。所以Runtime內(nèi)部實際使用的是OS提供的非阻塞IO訪問模式。那么如何將OS的異步I/O與Golang接口的阻塞I/O互相轉(zhuǎn)換呢?golang內(nèi)部就通過OS提供的非阻塞IO訪問模式、并配合epll/kqueue等IO事件監(jiān)控機(jī)制,通過runtime上做的一層封裝,實現(xiàn)將OS的異步I/O與Goroutine的阻塞 I/O互相轉(zhuǎn)換 。這一部分被稱之為netpoller
1. goroutine同步調(diào)用轉(zhuǎn)OS異步調(diào)用
當(dāng)一個goroutine進(jìn)行I/O操作時,并且文件描述符數(shù)據(jù)還沒有準(zhǔn)備好,經(jīng)過一系列的調(diào)用,最后會進(jìn)入gopark函數(shù),gopark將當(dāng)前正在執(zhí)行的goroutine狀態(tài)保存起來,然后切換到新的堆棧上執(zhí)行新的goroutine。由于當(dāng)前goroutine狀態(tài)是被保存起來的,因此后面可以被恢復(fù)。這樣進(jìn)行I/O操作的goroutine以為一直同步阻塞到現(xiàn)在,其實內(nèi)部是異步完成的。

2. goroutine什么時候調(diào)度回來
在schedule()執(zhí)行時,findrunnable()中的netpoll()方法被調(diào)用后,處于就緒狀態(tài)的 fd 對應(yīng)的 G 就會被調(diào)度回來。

8. scheduler affinity

goroutine之間使用channel來回通信時,會導(dǎo)致goroutine頻繁阻塞,導(dǎo)致其在本地隊列會進(jìn)行頻繁地重新排隊,導(dǎo)致goroutine存在被重排后有可能會被竊取的風(fēng)險。

Go 1.5 在 P 中引入了runnext 特殊的一個字段,當(dāng)一組goroutines在communicate-and-wait模式中被阻塞,但很快就runnable了,便會將runnext分別指向這組goroutines。在當(dāng)前G運行結(jié)束,之后將立即執(zhí)行runnext對應(yīng)的G,而不是本地隊列中的G。這允許 goroutine 在再次被阻塞之前能夠快速運行,提高了一部分性能。
六、goroutine的生命周期

- 流程步驟:
- runtime創(chuàng)建最初的線程m0和goroutine g0,并把2者關(guān)聯(lián)。
- 調(diào)度器初始化:初始化m0、棧、垃圾回收,以及創(chuàng)建和初始化P列表,P的數(shù)目優(yōu)先取環(huán)境變量GOMAXPROCS,否則默認(rèn)是cpu核數(shù)。隨后把第一個P(便于理解可以叫它p0)與m0進(jìn)行綁定,這樣m0就有他自己的p了,就有條件執(zhí)行后續(xù)的任務(wù)g了。
- m0的g0會執(zhí)行調(diào)度任務(wù)(runtime.newproc),創(chuàng)建一個g,g指向runtime.main()(還不是我們main包中的main),并放到p的本地隊列。這樣m0就已經(jīng)同時具有了任務(wù)g和p,什么條件都具備了。
runtime.main(): 啟動 sysmon 線程;啟動 GC 協(xié)程;執(zhí)行 init,即代碼中的各種 init 函數(shù);執(zhí)行 main.main 函數(shù)。
- 啟動m0,m0已經(jīng)綁定了P,會從P的本地隊列獲取g。
- g擁有棧,m根據(jù)g中的棧信息和調(diào)度信息設(shè)置運行環(huán)境
- M運行g(shù)
- g退出,再次回到M獲取可運行的G,這樣重復(fù)下去,直到main.main退出,runtime.main執(zhí)行Defer和Panic處理,或調(diào)用runtime.exit退出程序。
-
M0
M0是啟動程序后的編號為0的主線程,這個M對應(yīng)的實例會在全局變量runtime.m0中,不需要在heap上分配,M0負(fù)責(zé)執(zhí)行初始化操作和啟動第一個G, 在之后M0就和其他的M一樣了。 -
G0
G0是每次啟動一個M都會第一個創(chuàng)建的gourtine,G0僅負(fù)責(zé)調(diào)度,即shedule() 函數(shù), 每個M都會有一個自己的G0。在調(diào)度或系統(tǒng)調(diào)用時會使用G0的棧空間, 全局變量的G0是M0的G0。 -
G0的調(diào)度
以下3種場景,G0會執(zhí)行調(diào)度:
- 當(dāng)前 G 執(zhí)行完成,G0會執(zhí)行調(diào)度獲取下一個G
- 當(dāng)前 G 阻塞時:系統(tǒng)調(diào)用、互斥鎖或 chan,G0會執(zhí)行調(diào)度
- 在函數(shù)調(diào)用期間,如果當(dāng)前 G 必須擴(kuò)展其堆棧,G0會執(zhí)行調(diào)度
與常規(guī) G 相反,G0 有一個固定和更大的棧。G0除了負(fù)責(zé)G的調(diào)度,還有以下功能:
- Defer 函數(shù)的分配
- GC 收集,比如 STW、掃描 G 的堆棧和標(biāo)記、清除操作
- 棧擴(kuò)容,當(dāng)需要的時候,由 g0 進(jìn)行擴(kuò)棧操作

從 g 到 g0 或從 g0 到 g 的切換是相當(dāng)迅速的,它們只包含少量固定的指令。相反,對于schedule(),執(zhí)行schedule()需要檢查許多資源以便確定下一個要運行的 G。
g0調(diào)度具體流程:
- 當(dāng)前 g 阻塞在 chan 上并切換到 g0:
1、g的PC (程序計數(shù)器)和堆棧指針一起保存在內(nèi)部結(jié)構(gòu)中;
2、將 g0 設(shè)置為正在運行的 goroutine;
3、g0 的堆棧替換當(dāng)前堆棧; - g0 執(zhí)行schedule(),尋找runnable g
- g0 使用所選的 G 進(jìn)行切換:
1、PC 和堆棧指針是從G內(nèi)部結(jié)構(gòu)中獲取的;
2、程序跳轉(zhuǎn)到對應(yīng)的 PC 地址;
References:
http://www.cs.columbia.edu/~aho/cs6998/reports/12-12-11_DeshpandeSponslerWeiss_GO.pdf
https://www.yuque.com/aceld/golang/srxd6d
https://zhuanlan.zhihu.com/p/68299348
https://rakyll.org/scheduler/
https://zhuanlan.zhihu.com/p/27056944
https://www.cnblogs.com/sunsky303/p/11058728.html
https://yizhi.ren/2019/06/03/goscheduler
https://yizhi.ren/2019/06/08/gonetpoller
https://cloud.tencent.com/developer/article/1234360
https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine

