Golang排程器
本文儘量通俗易懂地講Go排程器(scheduler)的相關知識,尤其是普通開發者能夠關注和控制的部分。排程器本身十分複雜,所以下文難免有疏漏,發現後會儘量及時更新。
要點
- go程式的執行,以goroutine為單位,而goroutine實際執行在某個系統執行緒內。goroutine(可以非常多)和系統執行緒(相對比較少)並非一一對應。排程時,既有os排程執行緒,也有go排程器本身排程goroutine。簡言之,go原生支援併發,go排程器負責將各個goroutine排程到不同的作業系統執行緒中取執行。
- 三個定義:
- G: goroutine,就是平常提到的go中的協程
- P: process,處理器,有的文章說代表上下文,也可以理解為附帶有上下文資訊的令牌
- M: machine,執行緒,就是平常提到的作業系統中的執行緒
- Go早期是GM模型,後來因為效能問題轉而使用GPM模型
- 執行機制:
GOMAXPROCS
細節
-
GM到GPM
早期,GM模型有諸多問題,例如全域性鎖,M快取記憶體佔用浪費等,詳見《 Scalable Go Scheduler Design 》。因此,大神操刀加了一層中間層(P),排程模型變成GPM,沿用至今(盜圖一張):
GPM排程模型
G切換時,只是M從G1切到G2而已,都是在使用者態進行著,非常輕量,不像作業系統切換M時比較重。
P的本地佇列中缺少G時,會從其他P的佇列裡“偷”一些或者從全域性佇列裡取。
藉助於netpoller,發起網路呼叫時,G阻塞,M不阻塞,切換G即可。而發起檔案IO等操作時,會執行(阻塞的)系統呼叫,(注:現在應該實現了部分poller for os package),此時M也會等待系統呼叫的返回。M和G一起,會解除與P的繫結。如果P的本地佇列還有其他G,就會繫結另外一個空閒的M,如果沒有,則新建一個M,然後繼續執行可以執行的G。 -
排程器實現了搶佔
也就是說如果一個G執行太久,是會被切換出去的。
這樣可以確保整個程式看起來是“併發”執行的,而不是一個G可以執行時就是一直執行,其他G都餓死。
但是 切換點需要是函式呼叫 。假設G中是不調函式的純無限迴圈計算,還是無法被搶佔。
-
什麼時候G會被排程
- 被sysmon設定為搶佔
- channel阻塞或網路IO
- mutex等同步導致阻塞
- 使用go關鍵字建立goroutine
- GC過程中各種策略導致的排程
runtime中,網路IO的實現採用了kqueue (MacOS), epoll (Linux)或iocp (Windows) 。
-
檢視各種排程狀態
執行命令的時候,設定
GODEBUG
環境變數。例如:GODEBUG=schedtrace=1000 godoc -http=:6060
-
P有local佇列的好處
其實好處有好幾點。比較明顯的是,GM模型裡面,M切換G時,需要從全域性佇列裡面取,需要加鎖。GPM中,M繫結著P,M切換的G都在P的本地G佇列中,不需要鎖。
-
P預設是機器邏輯核數
因為超執行緒技術的存在,邏輯核數會與物理核數不同。下面的語句可以打印出邏輯核數,通過
GOMAXPROCS
設定時,可別弄錯了
fmt.Println(runtime.NumCPU())
-
M預設是10000
M對應的是
sched.maxmcount
,預設10000。通過SetMaxThreads
可修改,如果程式使用超過這個數,會自動crash!
// 改動時也會檢查,並不能隨意設定值 if in > 0x7fffffff { // MaxInt32 sched.maxmcount = 0x7fffffff } else { sched.maxmcount = int32(in) }
-
相關程式碼片段(不詳細研究的可以不用看了)
參見下一篇吧,這篇已很長了。
參考連結: