golang MPG併發模型
以上這張圖就是golang
的mpg
模型中各個元素的說明:
-
M
:物理線程,和其他語言中的線程是一致的;最大限制爲10000個 -
P
:邏輯處理器,負責調度協程;通常數量和CPU
數量一致 -
G
:即golang
中通過go
開啓的協程
協程和線程的區別
協程被稱作輕量級線程,在go
語言中有幾個優勢:
- 協程棧初始大小爲2k,遠小於線程的1M;且協程棧可以動態擴容,最大到1G
- 協程的切片是邏輯控制器
P
在語言級別(用戶空間)實現的,相比於系統級的線程切換消耗少很多
協程的調度
正常情況下P
會從自身的空閒隊列中取出一個G
來執行,在早期版本中golang
實現的是非搶佔式調用,只有遇到IO
、管道、runtime.Gosched()
等阻塞操作時纔會進行切換
協程本身無法是無法自行進行切換的,在G
遭遇到阻塞操作時,P
會將當前的M
脫離並同時綁定到一個新的線程M
上,而原本的線程M
則會繼續阻塞在原本G
的調用
除了和P
綁定的線程外,其他的線程主要是就是用來處理被阻塞的任務上的
協程隊列
go
語言中有一個全局協程隊列,使用go
開啓的新協程就會被放入這個隊列中、阻塞的M
執行完畢後也是將G
放入到這個全局,P
會定期從這裏拉取新的G
,
而每個P
又會自己維護一個G
隊列,在消費掉自身的G
後會先從全局隊列中拉取;如果沒有的話就從其他P
的隊列中偷取,每次偷一半
lua中的協程
lua
中的協程和go
語言的協程完全時不一樣的,lua
所有代碼運行在一個線程中,實際上並不是併發的;
lua
語言是不需要調度器P
的,主要是協程內部主動調用函數切換,本質其實是類似於函數調用
搶佔式調用
早期go
語言實現的是非搶佔式調用,這樣的問題在於
for{}
如果只有一個P
的情況下執行到上述代碼,程序就會永遠循環在這裏,其他協程再也無法執行到
更嚴重的問題是是,go
語言的垃圾回收是需要停止整個世界
的,如果某個協程永遠不停止,那麼垃圾回收就會一致等待
但是如果是搶佔式,那麼就會在切換任務時,保存當前的上下文環境,因爲當前線程如果正在做一件事,做到一半我們就強制停止,這時我們就必須多保存很多信息,避免再次切換回來時任務出錯,這是需要付出代價的
go語言實現的搶佔式調用是非常初級的,而且最終還是需要協程主動讓出才能切換
什麼時候需要搶佔式調用
- 執行時間過長的協程:防止其他協程餓死
- GC需要停止某個協程來進行棧掃描
- GC需要STW停止
整個世界
再進行工作
sysmon
在程序初始化的時候會創建一個後臺線程執行sysmon
,在程序執行期間每隔20us~10ms
執行一次,對於執行超過10ms
的協稱會打上標記,供後續進行切換
初次之外sysmon還需要處理gc、網絡輪詢器的邏輯
協程切換
在go1.13
版本前在如果sysmon
發現需要進行調度會在函數的棧寄存器中打一個標記,這也就意味着for{}
還是無法進行切換
在此之後是通過發送、監聽sigPreempt
信號實現的