作者:林冠巨集 / 指尖下的幽靈
GitHub : github.com/af913337456…
前序
正確地認識 G , M , P 三者的關係,能夠對協程的排程機制有更深入的理解! 本文將會完整介紹完 go 協程的排程機制,包含:
- 排程物件的主要組成
- 各物件的關係 與 分工
- gorutine 協程是如何被執行的
- 核心執行緒 sysmon 對 gorutine 的管理
- gorutine 協程中斷掛起 與 恢復
- GOMAXPROCS 如何影響 go 的併發效能
目錄
排程器的三個基本物件:
Golang 簡稱 Go,Go 的協程(goroutine)
和我們常見的執行緒(Thread)
一樣,擁有其排程器。
- G (Goroutine),代表協程,也就是每次程式碼中使用
go 關鍵詞
時候會建立的一個物件 - M (Work Thread),工作執行緒
- P (Processor),代表一個
處理器
,又稱上下文
G-M-P三者的關係與特點:
- 每一個執行的 M 都必須繫結一個 P,執行緒M 建立後會去檢查並執行G (goroutine)物件
- 每一個 P 儲存著一個協程G 的
佇列
- 除了每個 P 自身儲存的 G 的佇列外,排程器還擁有一個全域性的 G 佇列
- M 從
佇列中
提取 G,並執行 - P 的個數就是
GOMAXPROCS
(最大256),啟動時固定的,一般不修改 - M 的個數和 P 的個數不一定一樣多(會有休眠的M 或 P不繫結M )(最大10000)
- P 是用一個全域性陣列(255)來儲存的,並且維護著一個全域性的 P 空閒連結串列
區域性G佇列與全域性G佇列的關係
- 全域性G任務佇列會和各個本地G任務佇列按照一定的策略互相交換。沒錯,就是
協程任務
交換 - G任務的執行順序是,先從本地佇列找,本地沒有則從
全域性佇列
找 - 轉移
- 區域性與全域性,全域性G個數 / P個數
- 區域性與區域性,一次性轉移一半
Gorutine從入隊到執行
- 當我們建立一個G物件,就是
gorutine
,它會加入到本地佇列或者全域性佇列 - 如果還有空閒的P,則建立一個M 繫結該 P ,注意!這裡,P 此前必須還沒繫結過M 的,否則不滿足空閒的條件。細節點:
- 先找到一個空閒的P,如果沒有則直接返回
- P 個數不會佔用超過自己設定的cpu個數
- P 在被 M 繫結後,就會初始化自己的 G 佇列,此時是一個
空佇列
- 注意這裡的
一個點
!- 無論在哪個 M 中建立了一個 G,只要 P 有空閒的,就會引起新 M 的建立
- 不需考慮當前所在 M 中所綁的 P 的 G 佇列是否已滿
- 新建立的 M 所綁的 P 的初始化佇列會從其他 G 佇列中取任務過來
- 這裡留下第一個問題:
如果一個G任務執行時間太長,它就會一直佔用 M 執行緒,由於佇列的G任務是順序執行的,其它G任務就會阻塞,如何避免該情況發生? --①
- M 會啟動一個
底層執行緒
,迴圈執行
能找到的 G 任務。這裡的尋找的 G 從下面幾方面找:- 當前 M 所綁的 P 佇列中找
- 去別的 P 的佇列中找
- 去全域性 G 佇列中找
- G任務的執行順序是,先從本地佇列找,本地沒有則從全域性佇列找
- 程式啟動的時候,首先跑的是主執行緒,然後這個主執行緒會繫結第一個 P
- 入口 main 函式,其實是作為一個 goroutine 來執行
解答問題-①
協程的切換時間片是10ms,也就是說 goroutine 最多執行10ms就會被 M 切換到下一個 G。這個過程,又被稱為 中斷,掛起
原理:
go程式啟動時會首先建立一個特殊的核心執行緒 sysmon
,用來監控和管理,其內部是一個迴圈:
-
記錄所有 P 的 G 任務的
計數 schedtick
,schedtick會在每執行一個G任務後遞增 -
如果檢查到
schedtick
一直沒有遞增,說明這個 P 一直在執行同一個 G 任務,如果超過10ms,就在這個G任務的棧資訊裡面加一個 tag 標記 -
然後這個 G 任務在執行的時候,如果遇到非行內函數呼叫,就會檢查一次這個標記,然後中斷自己,把自己加到佇列末尾,執行下一個G
-
如果沒有遇到
非行內函數
呼叫的話,那就會一直執行這個G任務,直到它自己結束;如果是個死迴圈,並且 GOMAXPROCS=1 的話。那麼一直只會只有一個 P 與一個 M,且佇列中的其他 G 不會被執行!
例子,下面的這段程式碼,hello world
不會被輸出
func main(){
runtime.GOMAXPROCS(1)
go func(){
fmt.Println("hello world")
}()
go func(){
for {
}
}()
select {}
}
複製程式碼
中斷後的恢復
- 中斷的時候將暫存器裡的棧資訊,儲存到自己的 G 物件裡面
- 當再次輪到自己執行時,將自己儲存的棧資訊複製到暫存器裡面,這樣就接著上次之後運
GOMAXPROCS--效能調優
看完上面的內容,相信你已經知道,GOMAXPROCS
就是 go 中 runtime 包的一個函式。它設定了 P 的最多的個數。這也就直接導致了 M 最多的個數是多少,而 M 的個數就決定了各個 G 佇列能同時被多少個 M 執行緒來進行調取執行!
故,我們一般將 GOMAXPROCS 的個數設定為 CPU 的核數,且需要注意的是:
- go 1.5 版本之前的 GOMAXPROCS 預設是 1
- go 1.5 版本之後的 GOMAXPROCS 預設是 Num of cpu