Golang的Goruntine調度原理

Goroutine的調度模型

Go的調度器內部有四個重要的結構:M,P,S,Sched
(定義在源碼的src/runtime/runtime.h文件中)

一、G代表一個Goroutine對象,每次go調用的時候,都會創建一個G對象,它有自己的棧,Instruction Pointer和其他信息(正在等待的channel等等),用於調度。

二、M代表內核級線程,一個M就是一個線程,Goroutine就是跑在M之上的;M是一個很大的結構,裏面維護小對象內存cache(mcache)、當前執行的Goroutine、隨機數發生器等等非常多的信息。

三、P代表一個處理器,每一個運行的M都必須綁定一個P,就像線程必須在一個CPU核上執行一樣,它的主要用途就是用來執行Goroutine的,所以它也維護了一個Goroutine隊列,裏面存儲了所有需要它來執行的Goroutine。

四、Sched代表調度器,它維護存儲M和G的隊列以及調度器的一些狀態信息等。

調度實現

注意要點

  1. P的數量可以通過GOMAXPROCS()來設置,它其實也就代表了真正的併發度,即有多少個Goroutine可以同時運行(最大256)。

  2. 每一個P保存着本地G任務隊列(RunQueue),同時會有一個全局G任務隊列(Global RunQueue)。

每次go調用的時候

  1. 創建一個G對象,加入到本地隊列或者全局隊列

  2. 如果還有空閒的P,則創建一個M

  3. M會啓動一個底層線程,循環執行能找到的G任務

  4. G任務的執行順序是,先從本地隊列找,本地沒有則從全局隊列找,之後再去其它P中找(一次性轉移一半)

  5. 以上的G任務執行是按照隊列順序(也就是go調用的順序)執行的。
    在這裏插入圖片描述

當線程阻塞時

  1. 當一個OS線程M0陷入阻塞時,P轉而去運行M1,圖中的M1可能是正被創建,或者從線程緩存中取出。

  2. 當MO返回時,它必須嘗試取得一個P來運行Goroutine,一般情況下,它會從其他的OS線程那裏拿一個P過來。

  3. 如果沒有拿到的話,它就把Goroutine放在Global RunQueue裏,然後自己睡眠(放入線程緩存裏)。所有的P也會週期性的檢查Global RunQueue並運行其中的Goroutine,否則Global RunQueue上的Goroutine永遠無法執行。
    在這裏插入圖片描述

Goruntinue如何調度

系統在啓動的時候,會專門創建一個線程sysmon,用來監控和管理,在內部是一個循環:

  1. 記錄所有P的G任務計數schedtick,schedtick會在每執行一個G任務後遞增

  2. 如果檢查到 schedtick一直沒有遞增,說明這個P一直在執行同一個G任務,如果超過一定的時間(10ms),就在這個G任務的棧信息裏面加一個標記

  3. 當G任務在執行的時候,如果遇到非內聯函數調用,就會檢查一次這個標記,然後中斷自己,把自己加到隊列末尾,執行下一個G

  4. 如果沒有遇到非內聯函數(有時候正常的小函數會被優化成內聯函數)調用的話,那就慘了,會一直執行這個G任務,直到它自己結束;如果是個死循環,並且GOMAXPROCS=1的話,恭喜你,夯住了

  5. 所以Goroutine是按照搶佔式調度的,一個Goroutine最多執行10ms就會換作下一個

G任務中斷後的恢復

  1. 中斷的時候將寄存器裏的棧信息,保存到自己的G對象裏面

  2. 當再次輪到自己執行時,將自己保存的棧信息複製到寄存器裏面,這樣就接着上次之後運行了

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