前序
正確地認識 G , M , P 三者的關系,能夠對協程的調度機制有更深入的理解! 本文將會完整介紹完 go 協程的調度機制,包含:
- 調度對象的主要組成
- 各對象的關系 與 分工
- gorutine 協程是如何被執行的
- 內核線程 sysmon 對 gorutine 的管理
- gorutine 協程中斷掛起 與 恢復
- GOMAXPROCS 如何影響 go 的並發性能
調度器的三個基本對象:
Golang 簡稱 Go,Go 的協程(goroutine)
和我們常見的線程(Thread)
一樣,擁有其調度器。
- G (Goroutine),代表協程,也就是每次代碼中使用
go 關鍵詞
時候會創建的一個對象 - M (Work Thread),工作線程,一個M代表了一個內核線程,等同於系統線程,
- P (Processor),代表一個
處理器
,用來管理和執行goroutine,一個P代表了M所需的上下文環境
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需要綁定在M上才能運行,M需要綁定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 從下面幾方面找: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 不會被執行!
--------
線程分為內核態線程和用戶態線程,用戶態線程需要綁定內核態線程,CPU並不能感知用戶態線程的存在,它只知道它在運行1個線程,這個線程實際是內核態線程。
用戶態線程實際有個名字叫協程(co-routine),為了容易區分,我們使用協程指用戶態線程,使用線程指內核態線程。
協程跟線程是有區別的,線程由CPU調度是搶占式的,協程由用戶態調度是協作式的,一個協程讓出CPU后,才執行下一個協程。
調度器的有兩大思想:
復用線程:協程本身就是運行在一組線程之上,不需要頻繁的創建、銷毀線程,而是對線程的復用。在調度器中復用線程還有2個體現:1)work stealing,當本線程無可運行的G時,嘗試從其他線程綁定的P偷取G,而不是銷毀線程。2)hand off,當本線程因為G進行系統調用阻塞時,線程釋放綁定的P,把P轉移給其他空閑的線程執行。
利用並行:GOMAXPROCS設置P的數量,當GOMAXPROCS大於1時,就最多有GOMAXPROCS個線程處於運行狀態,這些線程可能分布在多個CPU核上同時運行,使得並發利用並行。另外,GOMAXPROCS也限制了並發的程度,比如GOMAXPROCS = 核數/2,則最多利用了一半的CPU核進行並行。
調度器的兩小策略:
搶占:在coroutine中要等待一個協程主動讓出CPU才執行下一個協程,在Go中,一個goroutine最多占用CPU 10ms,防止其他goroutine被餓死,這就是goroutine不同於coroutine的一個地方。全局G隊列:在新的調度器中依然有全局G隊列,但功能已經被弱化了,當M執行work stealing從其他P偷不到G時,它可以從全局G隊列獲取G。
全局G隊列:在新的調度器中依然有全局G隊列,但功能已經被弱化了,當M執行work stealing從其他P偷不到G時,它可以從全局G隊列獲取G。
摘自:https://cloud.tencent.com/developer/article/1442315