GPM 模型
[TOC]
參考:深入Golang排程器之GMP模型
前言
在瞭解 Go 的 gorutine 時,我們還是得先複習下,併發和並行的區別:
- 併發:同一段時間執行多個任務(你同時和兩個女朋友聊天)。
- 並行:同一時刻執行多個任務(你和你朋友都在和你女朋友聊天)。
在單核處理器上,通過多執行緒共享CPU時間片序列執行(併發非並行)。而並行則依賴於多核處理器等物理資源,讓多個任務可以實現並行執行(併發且並行)。
一、GPM的基本流程
1.1 GPM的含義
- G,表示一個 goroutine,即我需要分擔出去的任務;
- P,一個裝滿 G 的佇列,用於維護一些任務;
- M,一個操作器,用於將一個 G 搬到執行緒上執行;
1.2 Go排程器基本排程過程
- 建立一個 G 物件;
- 將 G 儲存至 P中;
- P 去喚醒(告訴)一個 M,然後繼續執行它的執行序(分配下一個 G);
- M 尋找空閒的 P,讀取該 P 要分配的 G;
- 接下來 M 執行一個排程迴圈,呼叫 G → 執行 → 清理執行緒 → 繼續找新的 G 執行。
簡單敘述各自的任務:
- G,攜帶任務;
- P,分配任務;
- M,尋找任務;
二、全面的流程
2.1 各自攜帶的資訊
-
G
- 需執行函式的指令(指標)
- 執行緒上下文的資訊(goroutine切換時,用於儲存 g 的上下文,例如,變數、相關資訊等)
- 現場保護和現場恢復(用於全域性佇列執行時的保護)
- 所屬的函式棧
- 當前執行的 m
- 被阻塞的時間
-
P,P/M需要進行繫結,構成一個執行單元。P決定了同時可以併發任務的數量,可通過GOMAXPROCS限制同時執行使用者級任務的作業系統執行緒。可以通過runtime.GOMAXPROCS進行指定。
- 狀態(空閒、執行...)
- 關聯的 m
- 可執行的 goroutine 的佇列
- 下一個 g
-
M,所有M是有執行緒棧的。如果不對該執行緒棧提供記憶體的話,系統會給該執行緒棧提供記憶體(不同作業系統提供的執行緒棧大小不同)。
- 所屬的排程棧
- 當前執行的 g
- 關聯的 p
- 狀態
以上列舉了三個結構各自的重要屬性,現在我們來看下詳細的執行流程。
2.2 準備知識
2.2.1 棧
普通棧:普通棧指的是需要排程的 goroutine 組成的函式棧,是可增長的棧,因為 goroutine 可以越開越多。
執行緒棧:執行緒棧是由需要將 goroutine 放置執行緒上的 m 們組成,實質上 m 也是由 goroutine 生成的,執行緒棧大小固定(設定了 m 的數量)。所有排程相關的程式碼,會先切換到該goroutine的棧中再執行。也就是說執行緒的棧也是用的g實現,而不是使用的OS的。
2.2.2 佇列
全域性佇列:該佇列儲存的 G 將被所有的 M 全域性共享,為保證資料競爭問題,需加鎖處理。
本地佇列:該佇列儲存資料資源相同的任務,每個本地佇列都會繫結一個 M ,指定其完成任務,沒有資料競爭,無需加鎖處理,處理速度遠高於全域性佇列。
2.2.3 上下文切換
簡單理解為當時的環境即可,環境可以包括當時程式狀態以及變數狀態。
對於程式碼中某個值說,上下文是指這個值所在的區域性(全域性)作用域物件。相對於程式而言,上下文就是程式執行時的環境,具體來說就是各個變數和資料,包括所有的暫存器變數、程式開啟的檔案、記憶體(堆疊)資訊等。
2.2.4 執行緒清理
由於每個P都需要繫結一個 M 進行任務執行,所以當清理執行緒的時候,只需要將 P 釋放(解除繫結)(M就沒有任務),即可。P 被釋放主要由兩種情況:
- 主動釋放:最典型的例子是,當執行G任務時有系統呼叫,當發生系統呼叫時M會處於阻塞狀態。排程器會設定一個超時時間,當超時時會將P釋放。
- 被動釋放:如果發生系統呼叫,有一個專門監控程式,進行掃描當前處於阻塞的P/M組合。當超過系統程式設定的超時時間,會自動將P資源搶走。去執行佇列的其它G任務。
阻塞是正在執行的執行緒沒有執行結束,暫時讓出 CPU。
2.2.5 搶佔式排程
在runtime.main
中會建立一個額外m執行sysmon
函式,搶佔就是在sysmon中實現的。
sysmon會進入一個無限迴圈, 第一輪迴休眠20us, 之後每次休眠時間倍增, 最終每一輪都會休眠10ms. sysmon中有netpool(獲取fd事件), retake(搶佔), forcegc(按時間強制執行gc), scavenge heap(釋放自由列表中多餘的項減少記憶體佔用)等處理。
搶佔條件:
- 如果 P 在系統呼叫中,且時長已經過一次 sysmon 後,則搶佔;
呼叫 handoffp
解除 M 和 P 的關聯。
- 如果 P 在執行,且時長經過一次 sysmon 後,並且時長超過設定的阻塞時長,則搶佔;
設定標識,標識該函式可以被中止,當呼叫棧識別到這個標識時,就知道這是搶佔觸發的, 這時會再檢查一遍是否要搶佔。
2.3 詳細流程
基本流程和上面一樣。每建立出一個 g,優先建立一個 p 進行儲存,當 p 達到限制後,則加入狀態為 waiting 的佇列中。
如果 g 執行時需要被阻塞,則會進行上下文切換,系統歸還資源後,再返回繼續執行。
當一個G長久阻塞在一個M上時,runtime會新建一個M,阻塞G所在的P會把其他的G 掛載在新建的M上。當舊的G阻塞完成或者認為其已經死掉時 回收舊的M(搶佔式排程)。
P會對自己管理的goroutine佇列做一些排程(比如把佔用CPU時間較長的goroutine暫停、執行後續的goroutine等等)當自己的佇列消費完了就去全域性佇列裡取,如果全域性佇列裡也消費完了會去其他P的佇列裡搶任務(所以需要單獨儲存下一個 g 的地址,而不是從佇列裡獲取)。
三、總結
相比大多數並行設計模型,Go比較優勢的設計就是P上下文這個概念的出現,如果只有G和M的對應關係,那麼當G阻塞在IO上的時候,M是沒有實際在工作的,這樣造成了資源的浪費,沒有了P,那麼所有G的列表都放在全域性,這樣導致臨界區太大,對多核排程造成極大影響。
而goroutine在使用上面的特點,感覺既可以用來做密集的多核計算,又可以做高併發的IO應用,做IO應用的時候,寫起來感覺和對程式設計師最友好的同步阻塞一樣,而實際上由於runtime的排程,底層是以同步非阻塞的方式在執行(即IO多路複用)。
所以說保護現場的搶佔式排程和G被阻塞後傳遞給其他m呼叫的核心思想,使得goroutine的產生。
單從執行緒排程講,Go語言相比起其他語言的優勢在於OS執行緒是由OS核心來排程的,goroutine
則是由Go執行時(runtime)自己的排程器排程的,這個排程器使用一個稱為m:n排程的技術(複用/排程m個goroutine到n個OS執行緒)。 其一大特點是goroutine的排程是在使用者態下完成的, 不涉及核心態與使用者態之間的頻繁切換,包括記憶體的分配與釋放,都是在使用者態維護著一塊大的記憶體池, 不直接呼叫系統的malloc函式(除非記憶體池需要改變),成本比排程OS執行緒低很多。 另一方面充分利用了多核的硬體資源,近似的把若干goroutine均分在物理執行緒上, 再加上本身goroutine的超輕量,以上種種保證了go排程方面的效能。