golang MPG併發模型 golang MPG併發模型

golang MPG併發模型

以上這張圖就是golangmpg模型中各個元素的說明:

  • 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信號實現的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章