1. 程式人生 > 實用技巧 >Golang協程排程原理( G、M、P)

Golang協程排程原理( G、M、P)

前序

正確地認識 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從入隊到執行

  1. 當我們建立一個G物件,就是 gorutine,它會加入到本地佇列或者全域性佇列
  2. 如果還有空閒的P,則建立一個M 繫結該 P ,注意!這裡,P 此前必須還沒繫結過M 的,否則不滿足空閒的條件。細節點:
    1. 先找到一個空閒的P,如果沒有則直接返回
    2. P 個數不會佔用超過自己設定的cpu個數
    3. P 在被 M 繫結後,就會初始化自己的 G 佇列,此時是一個空佇列
    4. 注意這裡的一個點
      • 無論在哪個 M 中建立了一個 G,只要 P 有空閒的,就會引起新 M 的建立
      • 不需考慮當前所在 M 中所綁的 P 的 G 佇列是否已滿
      • 新建立的 M 所綁的 P 的初始化佇列會從其他 G 佇列中取任務過來
    5. 這裡留下第一個問題: 如果一個G任務執行時間太長,它就會一直佔用 M 執行緒,由於佇列的G任務是順序執行的,其它G任務就會阻塞,如何避免該情況發生? --①
  3. M 會啟動一個底層執行緒迴圈執行能找到的 G 任務。這裡的尋找的 G 從下面幾方面找:G任務的執行順序是,先從本地佇列找,本地沒有則從全域性佇列找
    • 當前 M 所綁的 P 佇列中找
    • 去別的 P 的佇列中找
    • 去全域性 G 佇列中找
  4. 程式啟動的時候,首先跑的是主執行緒,然後這個主執行緒會繫結第一個 P
  5. 入口 main 函式,其實是作為一個 goroutine 來執行

解答問題-①

協程的切換時間片是10ms,也就是說 goroutine 最多執行10ms就會被 M 切換到下一個 G。這個過程,又被稱為 中斷,掛起

原理:

go程式啟動時會首先建立一個特殊的核心執行緒 sysmon,用來監控和管理,其內部是一個迴圈:

  1. 記錄所有 P 的 G 任務的計數 schedtick,schedtick會在每執行一個G任務後遞增
  2. 如果檢查到 schedtick 一直沒有遞增,說明這個 P 一直在執行同一個 G 任務,如果超過10ms,就在這個G任務的棧資訊裡面加一個 tag 標記
  3. 然後這個 G 任務在執行的時候,如果遇到非行內函數呼叫,就會檢查一次這個標記,然後中斷自己,把自己加到佇列末尾,執行下一個G
  4. 如果沒有遇到非行內函數 呼叫的話,那就會一直執行這個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