GMP
Goroutine調度是一個很復雜的機制,下面嘗試用簡單的語言描述一下Goroutine調度機制,想要對其有更深入的了解可以去研讀一下原始碼,
目錄- GMP
- 介紹
- 設計策略
- 復用執行緒
- 并行
- 搶占
- 全域goroutine佇列
- 協程經歷程序
- M緩沖池
- 觸發調度
- 佇列輪轉
- 特殊的 M0 和 G0
- 一個G由于調度被中斷,此后如何恢復?
- 總結
介紹
首先介紹一下GMP什么意思:
G ----------- goroutine: 即Go協程,每個go關鍵字都會創建一個協程,
M ---------- thread內核級執行緒,所有的G都要放在M上才能運行,
P ----------- processor處理器,調度G到M上,其維護了一個佇列,存盤了所有需要它來調度的G,
Goroutine 調度器P和 OS 調度器是通過 M 結合起來的,每個 M 都代表了 1 個內核執行緒,OS 調度器負責把內核執行緒分配到 CPU 的核上執行
模型圖:
設計策略
復用執行緒
避免頻繁的創建、銷毀執行緒,而是對執行緒的復用,
1)work stealing機制
當本執行緒無可運行的G時,嘗試從其他執行緒系結的P偷取G,而不是銷毀執行緒,
2)hand off機制
當本執行緒M0因為G0進行系統呼叫阻塞時,執行緒釋放系結的P,把P轉移給其他空閑的執行緒執行,進而某個空閑的M1獲取P,繼續執行P佇列中剩下的G,而M0由于陷入系統呼叫而進被阻塞,M1接替M0的作業,只要P不空閑,就可以保證充分利用CPU,M1的來源有可能是M的快取池,也可能是新建的,當G0系統呼叫結束后,根據M0是否能獲取到P,將會將G0做不同的處理:
- 如果有空閑的P,則獲取一個P,繼續執行G0,
- 如果沒有空閑的P,則將G0放入全域佇列,等待被其他的P調度,然后M0將進入快取池睡眠,
如下圖
并行
GOMAXPROCS設定P的數量,最多有GOMAXPROCS個執行緒分布在多個CPU上同時運行
搶占
在Go中一個goroutine最多占用CPU 10ms,防止其他goroutine被餓死,
具體可以去看另一篇文章
【Golang詳解】go語言調度機制 搶占式調度
全域goroutine佇列
當創建一個新的G之后優先加入本地佇列,如果本地佇列滿了,會將本地佇列的G移動到全域佇列里面,當M執行work stealing從其他P偷不到G時,它可以從全域G佇列獲取G,
協程經歷程序
我們創建一個協程 go func()經歷程序如下圖:
說明:
這里有兩個存盤G的佇列,一個是區域調度器P的本地佇列、一個是全域G佇列,新創建的G會先保存在P的本地佇列中,如果P的本地佇列已經滿了就會保存在全域的佇列中;處理器本地佇列是一個使用陣列構成的環形鏈表,它最多可以存盤 256 個待執行任務,
G只能運行在M中,一個M必須持有一個P,M與P是1:1的關系,M會從P的本地佇列彈出一個可執行狀態的G來執行,如果P的本地佇列為空,就會想其他的MP組合偷取一個可執行的G來執行;
一個M調度G執行的程序是一個回圈機制;會一直從本地佇列或全域佇列中獲取G
M緩沖池
上面說到P的個數默認等于CPU核數,每個M必須持有一個P才可以執行G,一般情況下M的個數會略大于P的個數,這多出來的M將會在G產生系統呼叫時發揮作用,類似執行緒池,Go也提供一個M的池子,需要時從池子中獲取,用完放回池子,不夠用時就再創建一個,
觸發調度
work-stealing調度演算法:當M執行完了當前P的本地佇列佇列里的所有G后,P也不會就這么在那躺尸啥都不干,它會先嘗試從全域佇列佇列尋找G來執行,如果全域佇列為空,它會隨機挑選另外一個P,從它的佇列里中拿走一半的G到自己的佇列中執行,
如果一切正常,調度器會以上述的那種方式順暢地運行,但這個世界沒這么美好,總有意外發生,以下分析goroutine在兩種例外情況下的行為,
Go runtime會在下面的goroutine被阻塞的情況下運行另外一個goroutine:
用戶態阻塞/喚醒
- 當goroutine因為channel操作或者network I/O而阻塞時(實際上golang已經用netpoller實作了goroutine網路I/O阻塞不會導致M被阻塞,僅阻塞G,這里僅僅是舉個栗子),對應的G會被放置到某個wait佇列(如channel的waitq),該G的狀態由
_Gruning變為_Gwaitting,而M會跳過該G嘗試獲取并執行下一個G,如果此時沒有可運行的G供M運行,那么M將解綁P,并進入sleep狀態;當阻塞的G被另一端的G2喚醒時(比如channel的可讀/寫通知),G被標記為,嘗試加入G2所在P的runnext(runnext是執行緒下一個需要執行的 Goroutine,), 然后再是P的本地佇列和全域佇列,
系統呼叫阻塞
- 當M執行某一個G時候如果發生了阻塞操作,M會阻塞,如果當前有一些G在執行,調度器會把這個執行緒M從P中摘除,然后再創建一個新的作業系統的執行緒(如果有空閑的執行緒可用就復用空閑執行緒)來服務于這個P,當M系統呼叫結束時候,這個G會嘗試獲取一個空閑的P執行,并放入到這個P的本地佇列,如果獲取不到P,那么這個執行緒M變成休眠狀態, 加入到空閑執行緒中,然后這個G會被放入全域佇列中,
佇列輪轉
可見每個P維護著一個包含G的佇列,不考慮G進入系統呼叫或IO操作的情況下,P周期性的將G調度到M中執行,執行一小段時間,將背景關系保存下來,然后將G放到佇列尾部,然后從佇列中重新取出一個G進行調度,
除了每個P維護的G佇列以外,還有一個全域的佇列,每個P會周期性地查看全域佇列中是否有G待運行并將其調度到M中執行,全域佇列中G的來源,主要有從系統呼叫中恢復的G,之所以P會周期性地查看全域佇列,也是為了防止全域佇列中的G被餓死,
除了每個P維護的G佇列以外,還有一個全域的佇列,每個P會周期性地查看全域佇列中是否有G待運行并將其調度到M中執行,全域佇列中G的來源,主要有從系統呼叫中恢復的G,之所以P會周期性地查看全域佇列,也是為了防止全域佇列中的G被餓死,
特殊的 M0 和 G0
M0
M0是啟動程式后的編號為0的主執行緒,這個M對應的實體會在全域變數rutime.m0中,不需要在heap上分配,M0負責執行初始化操作和啟動第一個G,在之后M0就和其他的M一樣了
G0
G0是每次啟動一個M都會第一個創建的goroutine,G0僅用于負責調度G,G0不指向任何可執行的函式,每個M都會有一個自己的G0,在調度或系統呼叫時會使用G0的堆疊空間,全域變數的G0是M0的G0
一個G由于調度被中斷,此后如何恢復?
中斷的時候將暫存器里的堆疊資訊,保存到自己的G物件里面,當再次輪到自己執行時,將自己保存的堆疊資訊復制到暫存器里面,這樣就接著上次之后運行了,
總結
我這里只是根據自己的理解進行了簡單的介紹,想要詳細了解有關GMP的底層原理可以去看Go調度器 G-P-M 模型的設計者的檔案或直接看原始碼
參考:
(https://www.cnblogs.com/X-knight/p/11365929.html)
(https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/)
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/300159.html
標籤:其他
上一篇:用 Python 增強 Git
