1. 程式人生 > >[轉帖]go的排程機制.

[轉帖]go的排程機制.

排程器

主要基於三個基本物件上,G,M,P(定義在原始碼的src/runtime/runtime.h檔案中)

  1. G代表一個goroutine物件,每次go呼叫的時候,都會建立一個G物件
  2. M代表一個執行緒,每次建立一個M的時候,都會有一個底層執行緒建立;所有的G任務,最終還是在M上執行
  3. P代表一個處理器,每一個執行的M都必須繫結一個P,就像執行緒必須在麼一個CPU核上執行一樣

P的個數就是GOMAXPROCS(最大256),啟動時固定的,一般不修改;
M的個數和P的個數不一定一樣多(會有休眠的M或者不需要太多的M)(最大10000);每一個P儲存著本地G任務佇列,也有一個全域性G任務佇列;
如下圖所示

 

   

全域性G任務佇列會和各個本地G任務佇列按照一定的策略互相交換(滿了,則把本地佇列的一半送給全域性佇列)
P是用一個全域性陣列(255)來儲存的,並且維護著一個全域性的P空閒連結串列

每次go呼叫的時候,都會:

  1. 建立一個G物件,加入到本地佇列或者全域性佇列
  2. 如果還有空閒的P,則建立一個M
  3. M會啟動一個底層執行緒,迴圈執行能找到的G任務
  4. G任務的執行順序是,先從本地佇列找,本地沒有則從全域性佇列找(一次性轉移(全域性G個數/P個數)個,再去其它P中找(一次性轉移一半),
  5. 以上的G任務執行是按照佇列順序(也就是go呼叫的順序)執行的。(這個地方是不是覺得很奇怪??)

對於上面的第2-3步,建立一個M,其過程:

  • 先找到一個空閒的P,如果沒有則直接返回,(哈哈,這個地方就保證了程序不會佔用超過自己設定的cpu個數)
  • 呼叫系統api建立執行緒,不同的作業系統,呼叫不一樣,其實就是和c語言建立過程是一致的,(windows用的是CreateThread,linux用的是clone系統呼叫)
  • 然後建立的這個執行緒裡面才是真正做事的,迴圈執行G任務

那就會有個問題,如果一個系統呼叫或者G任務執行太長,他就會一直佔用這個執行緒,由於本地佇列的G任務是順序執行的,其它G任務就會阻塞了,怎樣中止長任務的呢?

這樣滴,啟動的時候,會專門建立一個執行緒sysmon,用來監控和管理,在內部是一個迴圈:

  1. 記錄所有P的G任務計數schedtick,(schedtick會在每執行一個G任務後遞增)
  2. 如果檢查到 schedtick一直沒有遞增,說明這個P一直在執行同一個G任務,如果超過一定的時間(10ms),就在這個G任務的棧資訊裡面加一個標記
  3. 然後這個G任務在執行的時候,如果遇到非行內函數呼叫,就會檢查一次這個標記,然後中斷自己,把自己加到佇列末尾,執行下一個G
  4. 如果沒有遇到非行內函數(有時候正常的小函式會被優化成行內函數)呼叫的話,那就慘了,會一直執行這個G任務,直到它自己結束;如果是個死迴圈,並且GOMAXPROCS=1的話,恭喜你,夯住了!親測,的確如此

對於一個G任務,中斷後的恢復過程:

  • 中斷的時候將暫存器裡的棧資訊,儲存到自己的G物件裡面
  • 當再次輪到自己執行時,將自己儲存的棧資訊複製到暫存器裡面,這樣就接著上次之後運行了。

但是還有一個問題,就是系統啟動的過程

  1. 系統啟動的時候,首先跑的是主執行緒,那第一個M應該就是主執行緒吧(按照C語言的理解,嘿嘿),這裡叫M1,可以看前面的圖
  2. 然後這個主執行緒會繫結第一個P1
  3. 咱們寫的main函式,其實是作為一個goroutine來執行的
  4. 也就是第一個P1就有了一個G1任務,然後第一個M1就執行這個G1任務(也就是main函式),建立這個G1的時候不用建立M了,因為已經有了M1
  5. 這個main函式裡面所有的goroutine,都繫結到當前的M1所對應的P1上
  6. 然後建立main裡的goroutine的時候(比如G2),就會建立新的M2,新的M2裡的初始P2的本地任務佇列是空的,會從P1裡面取一些過來,哈哈
  7. 這樣兩個M1,M2各自執行自己的G任務,再依次往復,這下就圓滿了

綜上:
所以goroutine是按照搶佔式排程的,一個goroutine最多執行10ms就會換作下一個
這個和目前主流系統的的cpu排程類似(按照時間分片)

windows:20ms

linux:5ms-800ms

注意:
在Golang中編譯器也會嘗試進行內聯,將小函式直接複製並編譯,為了內聯,儘量消除編譯器無法偵測的dead code,利用gobuild -gcflags=-m編譯命令可以檢視程式內聯狀態,不得不說golang的編譯工具鏈還是很強大的,十分有利於程式的優化。