Go調度器: M,P和G
這是另一篇關于Go調度器(scheduler)的文章。 原文: GO SCHEDULER: MS, PS & GS by Uber工程師 Povilas。
網上已經有很多關于Go調度器的文章了, 比如 Golang調度器源碼分析 ,多看一些,可以加深記憶,也可以對比查看文章中是否有不準確的地方,更全面的了解Go的調度器。
我決定深入了解Go的內部機制, 因為很長時間沒人寫關于Go scheduler的文章了, 我覺得這是一個很有趣的知識點,所以讓我們開始吧。
基礎知識
Go的運行時管理著調度、垃圾回收以及goroutine的運行環境。本文只關注于調度器。
運行時負責運行goroutine并把它們影射到操作系統的線程上。goroutine比線程還輕量, 啟動的時候花費很少。每個goroutine都是由一個 G 結構來表示,
這個結構體的字段用來跟蹤此goroutine的棧(stack)和狀態,所以你可以認為 G = goroutine 。
運行時管理著 G 并把它們映射到 Logical Processor (稱之為 P ). P 可以看作是一個抽象的資源或者一個上下文,它需要獲取以便操作系統線程(稱之為 M )可以運行 G 。
通過 runtime.GOMAXPROCS (numLogicalProcessors) 可以控制多少 P 可以獲取。如果你需要調整這個參數(大部分情況下你無需調整), 只設置一次, 因為它需要 STW gc pause。
本質上,操作系統運行線程,線程運行你的代碼。Go的技巧是編譯器會在Go運行時的一些地方插入系統調用, (比如通過channel發送值,調用runtime包等),所以Go可以通知調度器執行特定的操作。
上圖的理解來自 Analysis of the Go runtime scheduler
M、P 和 G 之間的交互
M、 P 和 G 之間的交互有點復雜。看看下面這張來自 Gao Chao的 go runtime scheduler 幻燈片中的一張圖:
可以看到,Go運行時存在兩種類型的queue: 一種是一個全局的queue(在 schedt結構體中 ,很少用到), 一種是每個 P 都維護自己的 G 的queue。
為了運行goroutine, M 需要持有上下文 P 。 M 會從 P 的queue彈出一個goroutine并執行。
當你創建一個新的goroutine的時候( go func() 方法),它會被放入 P 的queue。當然還有一個 work-stealing 調度算法,當 M 執行了一些 G 后,如果它的queue為空,它會隨機的選擇另外一個 P ,從它的queue中取走一半的 G 到自己的queue中執行。(偷!)
當你的goroutine執行阻塞的系統調用的時候(syscall),阻塞的系統調用會中斷(intercepted),如果當前有一些 G 在執行,運行時會把這個線程從 P 中摘除(detach),然后再創建一個新的操作系統的線程(如果沒有空閑的線程可用的話)來服務于這個 P 。
當系統調用繼續的時候,這個goroutine被放入到本地運行queue,線程會 park 它自己(休眠), 加入到空閑線程中。
如果一個goroutine執行網絡調用,運行時會做類似的動作。調用會被中斷,但是由于Go使用集成的network poller,它有自己的線程,所以還給它。
Go運行時會在下面的goroutine被阻塞的情況下運行另外一個goroutine:
- blocking syscall (for example opening a file),
- network input,
- channel operations,
- primitives in the sync package.
調度器跟蹤調試
Go可以跟蹤運行時的調度器,這是通過 GODEBUG 環境變量實現的:
- $ GODEBUG=scheddetail=1,schedtrace=1000 ./program
下面是輸出的例子:
- SCHED 0ms: gomaxprocs=8 idleprocs=7 threads=2 spinningthreads=0 idlethreads=0 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
- P0: status=1 schedtick=0 syscalltick=0 m=0 runqsize=0 gfreecnt=0
- P1: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
- P2: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
- P3: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
- P4: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
- P5: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
- P6: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
- P7: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
- M1: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1
- M0: p=0 curg=1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=1
- G1: status=8() m=0 lockedm=0
注意輸出使用了 G 、 M 和 P 的概念以及她們的狀態, 比如 P 的queue的大小。 如果你不想關心這些細節,你可以使用:
- $ GODEBUG=schedtrace=1000 ./program
William Kennedy寫了一篇很好的 文章 , 解釋了這些細節。
當然,還有一個go自己的工具 go tool trace , 它有一個UI, 允許你查看你的程序和運行時的狀況。你可以閱讀這篇文章: Pusher 。