1. 引言
在上一篇文章中,我們宏觀地了解了 Go 的調度策略。現在,我們將深入到構成這個調度系統的三大核心組件:G、M、P。理解 GMP 模型是徹底搞懂 Go 并發調度原理的關鍵。
本文將詳細解析 G、M、P 各自的職責以及它們之間是如何協同工作的。
2. GMP 核心組件
G (Goroutine):
定義:G 就是我們常說的 Goroutine。它是一個待執行的任務單元,包含了執行所需的棧空間、指令指針 (PC) 以及其他狀態信息(如
goroutine status
)。特點:G 是輕量級的,初始棧大小僅為 2KB,可以根據需要動態伸縮。Go 程序可以輕松創建成千上萬個 G。
狀態:G 有多種狀態,如
_Gidle
(閑置),_Grunnable
(可運行),_Grunning
(運行中),_Gsyscall
(系統調用中),_Gwaiting
(等待中),_Gdead
(已死亡) 等。調度器根據這些狀態來移動 G。
M (Machine):
定義:M 是內核線程的抽象,是真正執行代碼的實體。
runtime
會限制 M 的數量,默認不超過 10000。職責:M 從一個關聯的 P 的本地隊列中獲取 G,然后執行 G 的代碼。如果 G 發生系統調用或阻塞,M 可能會與 P 解綁。
M0
與M
:M0
是主線程,是程序啟動時創建的第一個內核線程。
P (Processor):
定義:P 是處理器的抽象,它代表了 M 執行 Go 代碼所需的上下文和資源。P 的數量在程序啟動時被設置為
GOMAXPROCS
的值,通常等于 CPU 的核心數。職責:P 是 G 和 M 之間的“中間人”。它維護一個本地可運行 G 隊列 (LRQ),為 M 提供可執行的 G。P 的存在使得調度器可以控制并發的程度(即同時有多少個 G 在真正地運行)。
關鍵作用:P 的引入實現了 M 和 G 的解耦。當一個 M 因為其上運行的 G 進行系統調用而阻塞時,P 會從這個 M 上解綁,并去尋找一個空閑的 M(或創建一個新的 M)來繼續執行自己隊列中的其他 G。這使得 Go 的并發能力不會因為部分 Goroutine 的阻塞而受限。
3. GMP 的協作流程
一個典型的調度場景如下:
啟動:程序啟動,創建 M0,并創建
GOMAXPROCS
個 P。G 的創建:
go func()
創建一個新的 G。這個新的 G 會被優先放入當前 M 綁定的 P 的本地隊列 (LRQ) 的隊頭。M 的執行循環:
M 啟動后,會綁定一個 P(
acquirep
)。M 進入一個無限的調度循環 (
schedule
)。在循環中,M 嘗試從 P 的 LRQ、全局隊列 (GRQ) 或通過工作竊取 (
runqsteal
) 來獲取一個可運行的 G。找到 G 后,M 會執行 G 的代碼 (
execute
)。G 執行完畢后,M 會再次進入調度循環尋找下一個 G。
系統調用場景 (Syscall):
假設 M0 上的 G0 準備進行一個會阻塞的系統調用。
M0 會與它的 P0 解綁。
P0 會去尋找一個空閑的 M(比如 M1)或者創建一個新的 M1。
P0 會綁定到 M1 上,并繼續執行 P0 本地隊列中的其他 G。
與此同時,原來的 M0 則陷入系統調用,等待其完成。
當 G0 的系統調用結束后,它會嘗試獲取一個空閑的 P 來繼續執行。如果獲取不到,G0 會被放入全局隊列。M0 則會進入休眠。
這個設計使得 P(并發的許可證)不會因為 M 的阻塞而被浪費,從而保證了 GOMAXPROCS
個 Goroutine 可以持續地、并行地運行。
4. sysmon
監控線程
除了 G, M, P,還有一個重要的后臺角色:sysmon
。它是一個由 runtime
啟動的、不需要 P 的 M,在后臺執行以下任務:
搶占:如上篇所述,向運行時間過長的 G 發送搶占信號。
網絡輪詢 (netpoller):檢查網絡 I/O 是否就緒,并喚醒因此阻塞的 G。
GC 輔助:在需要時觸發 GC。
釋放閑置資源:定期將長時間空閑的 M 和 P 的資源回收。
5. 總結
GMP 模型是 Go 語言高性能并發調度的核心。
G 是任務單元。
M 是執行實體(內核線程)。
P 是調度上下文和資源的提供者,是實現 M:N 模型的關鍵。
它們通過工作竊取、系統調用處理、異步搶占等機制緊密協作,構成了一個強大、高效、自適應的調度系統。