如何更直观地理解 Go 调度过程
得益于 Go 語言優秀的運行時調度系統,即使開發人員沒有多線程編程經驗,也能很容易地開發并發程序。
調度系統,其中最核心的就是 GMP 的設計,欲深入理解 Go 語言設計的讀者都應該看過這些知識。但是,在通過相關博客或者源碼學習時,如果不能和實際的代碼進行結合,在理解上或許不夠深刻。
本文介紹一種方式,即使用 GODEBUG 工具,通過實際運行代碼來直觀地查看 Go 運行時的調度過程。
調度簡述
首先,我們先通過程序某時刻的調度快照示意圖,通過解析快照狀態來簡單回顧一下 Go 調度系統。
如上圖所示,我們設定了 GOMAXPROCS=2,即 2 個處理器。
當前時刻, P0 和 P1 上正分別掛載著 OS 線程 M1 與 M4,其上分別執行著 G8 和 G17 的代碼。P0 的 LRQ(Local Run Queue,本地運行隊列)有 3 個 G 在排隊等待,而 P1 的 LRQ 已無等待的 G;GRQ (Global Run Queue,全局運行隊列)中有 5 個 G 。
網絡輪詢器 Net Poller 上有一個陷入異步網絡調用的 G9;M2 由于 G11 的某種同步系統調用而阻塞;M3 處于空閑狀態,時刻準備著當 M1 或 M4 被阻塞時而派上用場。
由于 P1 的 LRQ 已無等待的 G,當 G17 被調度時,它將進行 Wrok Stealing (任務竊取),其竊取源來自于其他處理器 P 的 LRQ(這里是 P1 的 LRQ)、GRQ 和 Net Poller,具體規則見runtime.schedule()函數。
GODEBUG 工具
啟用 GODEBUG 工具非常簡單,只需要設置環境變量 GODEBUG 即可。它可以讓 Go 程序在運行過程中輸出調試信息,能夠根據參數配置直觀地看到調度器或垃圾回收等詳細信息。
GODEBUG 的詳細描述介紹可見源碼runtime/extern.go文件。
本文我們關心的調試內容是調度器,因此我們只使用 GODEBUG 的兩個參數 schedtrace 與 scheddetail。
schedtrace=n:設置運行時在每 n 毫秒輸出一行調度器的概要信息。
scheddetail: 輸出更詳細的調度信息。
示例代碼
我們使用的示例代碼如下
package?mainimport?("sync" )var?wg?sync.WaitGroupfunc?main()?{for?i?:=?0;?i?<?20;?i++?{wg.Add(1)go?work(&wg)}wg.Wait() }func?work(wg?*sync.WaitGroup)?{var?counter?intfor?i?:=?0;?i?<?1e10;?i++?{counter++}wg.Done() }代碼比較簡單,我們啟動 20 個 CPU 密集型的 G 任務,它們受到 WaitGroup 的限制。當所有計算任務的 G 完成了各自的累加工作,程序才會結束執行。
schedtrace 調度概要輸出
下面,我們設定 GODEBUG=schedtrace=1000,這意味著 1s 輸出一次程序的調度概要情況。
$?go?build?-o?demo?main.go$?GOMAXPROCS=4?GODEBUG=schedtrace=1000?./demo SCHED?0ms:?gomaxprocs=4?idleprocs=3?threads=2?spinningthreads=0?idlethreads=0?runqueue=0?[0?0?0?0] SCHED?1003ms:?gomaxprocs=4?idleprocs=0?threads=5?spinningthreads=0?idlethreads=0?runqueue=14?[1?0?1?0] SCHED?2012ms:?gomaxprocs=4?idleprocs=0?threads=5?spinningthreads=0?idlethreads=0?runqueue=10?[2?1?2?1] SCHED?3018ms:?gomaxprocs=4?idleprocs=0?threads=5?spinningthreads=0?idlethreads=0?runqueue=12?[0?0?0?4] SCHED?4029ms:?gomaxprocs=4?idleprocs=0?threads=5?spinningthreads=0?idlethreads=0?runqueue=15?[0?0?1?0] SCHED?5031ms:?gomaxprocs=4?idleprocs=0?threads=5?spinningthreads=0?idlethreads=0?runqueue=9?[1?2?2?2] SCHED?6035ms:?gomaxprocs=4?idleprocs=0?threads=5?spinningthreads=0?idlethreads=0?runqueue=15?[0?1?0?0] SCHED?7044ms:?gomaxprocs=4?idleprocs=0?threads=5?spinningthreads=0?idlethreads=0?runqueue=8?[1?2?3?2] SCHED?8054ms:?gomaxprocs=4?idleprocs=0?threads=5?spinningthreads=0?idlethreads=0?runqueue=12?[0?0?4?0] SCHED?9055ms:?gomaxprocs=4?idleprocs=0?threads=5?spinningthreads=0?idlethreads=0?runqueue=6?[3?2?3?2] SCHED?10063ms:?gomaxprocs=4?idleprocs=0?threads=5?spinningthreads=0?idlethreads=0?runqueue=11?[1?2?1?1] SCHED?11072ms:?gomaxprocs=4?idleprocs=0?threads=5?spinningthreads=0?idlethreads=0?runqueue=6?[3?2?3?2] ...其中,
SCHED:代表程序啟動到輸出當前行時的運行時間,這個輸出間隔受到 schedtrace 值影響。
gomaxprocs:GOMAXPROCS 值,這里我們設定了其為 4。
idleprocs:空閑的 P 數量。
threads:運行時管理的線程數。
spinningthreads:自旋線程,處于”自旋“狀態的線程數(避免頻繁的線程創建與銷毀)。
idlethreads:空閑線程數。
runqueue:全局隊列 GRQ 中的 G 數量。
[2 1 2 1]:代表 4 個 P 的本地隊列 LRQ 中 G 數量分別是 2、1、2、1 。
scheddetail 調度詳細輸出
當我們想要查看更詳細的調度信息時,需要增加 scheddetail 參數。
$?GOMAXPROCS=4?GODEBUG=schedtrace=1000,scheddetail=1?./demo SCHED?0ms:?gomaxprocs=4?idleprocs=2?threads=3?spinningthreads=1?idlethreads=0?runqueue=0?gcwaiting=0?nmidlelocked=0?stopwait=0?sysmonwait=0P0:?status=1?schedtick=0?syscalltick=0?m=0?runqsize=0?gfreecnt=0?timerslen=0P1:?status=1?schedtick=0?syscalltick=0?m=2?runqsize=0?gfreecnt=0?timerslen=0P2:?status=0?schedtick=0?syscalltick=0?m=-1?runqsize=0?gfreecnt=0?timerslen=0P3:?status=0?schedtick=0?syscalltick=0?m=-1?runqsize=0?gfreecnt=0?timerslen=0M2:?p=1?curg=-1?mallocing=0?throwing=0?preemptoff=?locks=2?dying=0?spinning=false?blocked=false?lockedg=-1M1:?p=-1?curg=-1?mallocing=0?throwing=0?preemptoff=?locks=2?dying=0?spinning=false?blocked=false?lockedg=-1M0:?p=0?curg=-1?mallocing=0?throwing=0?preemptoff=?locks=1?dying=0?spinning=false?blocked=false?lockedg=1G1:?status=1()?m=-1?lockedm=0G2:?status=1()?m=-1?lockedm=-1G3:?status=1()?m=-1?lockedm=-1 SCHED?1000ms:?gomaxprocs=4?idleprocs=0?threads=5?spinningthreads=0?idlethreads=0?runqueue=15?gcwaiting=0?nmidlelocked=0?stopwait=0?sysmonwait=0P0:?status=1?schedtick=45?syscalltick=0?m=2?runqsize=0?gfreecnt=0?timerslen=0P1:?status=1?schedtick=46?syscalltick=0?m=3?runqsize=0?gfreecnt=0?timerslen=0P2:?status=1?schedtick=45?syscalltick=0?m=4?runqsize=1?gfreecnt=0?timerslen=0P3:?status=1?schedtick=45?syscalltick=0?m=0?runqsize=0?gfreecnt=0?timerslen=0M4:?p=2?curg=-1?mallocing=0?throwing=0?preemptoff=?locks=1?dying=0?spinning=false?blocked=false?lockedg=-1M3:?p=1?curg=-1?mallocing=0?throwing=0?preemptoff=?locks=1?dying=0?spinning=false?blocked=false?lockedg=-1M2:?p=0?curg=40?mallocing=0?throwing=0?preemptoff=?locks=0?dying=0?spinning=false?blocked=false?lockedg=-1M1:?p=-1?curg=-1?mallocing=0?throwing=0?preemptoff=?locks=2?dying=0?spinning=false?blocked=false?lockedg=-1M0:?p=3?curg=-1?mallocing=0?throwing=0?preemptoff=?locks=1?dying=0?spinning=false?blocked=false?lockedg=-1G1:?status=4(semacquire)?m=-1?lockedm=-1G2:?status=4(force?gc?(idle))?m=-1?lockedm=-1G3:?status=4(GC?sweep?wait)?m=-1?lockedm=-1G17:?status=4(GC?scavenge?wait)?m=-1?lockedm=-1G33:?status=1()?m=-1?lockedm=-1G34:?status=1()?m=-1?lockedm=-1G35:?status=1()?m=-1?lockedm=-1G36:?status=1()?m=-1?lockedm=-1G37:?status=1()?m=-1?lockedm=-1G38:?status=1()?m=-1?lockedm=-1G39:?status=1()?m=-1?lockedm=-1G40:?status=2()?m=2?lockedm=-1G41:?status=1()?m=-1?lockedm=-1G42:?status=1()?m=-1?lockedm=-1G43:?status=1()?m=-1?lockedm=-1G44:?status=1()?m=-1?lockedm=-1G45:?status=1()?m=-1?lockedm=-1G46:?status=1()?m=-1?lockedm=-1G47:?status=1()?m=-1?lockedm=-1G48:?status=1()?m=-1?lockedm=-1G49:?status=1()?m=-1?lockedm=-1G50:?status=1()?m=-1?lockedm=-1G51:?status=1()?m=-1?lockedm=-1G52:?status=1()?m=-1?lockedm=-1 ...當增加了 scheddetail 參數后,其輸出信息不僅包含了 SCHED 的一行概覽信息,還增加了 GPM 三個實體狀況的詳細描述。
P
status:P 的運行狀態,其詳細分類與描述可查看源碼 runtime/runtime2.go 的代碼。
schedtick:隨著每次調度行為累加,代表 P 的調度次數。
syscalltick:隨著每次系統調用行為累加,代表 P 的系統調用次數。
m: 綁定的 M 編號,例如在 SCHED 1000ms 時,P0 的 m=2,而 M2 的 p=0。
runqsize:LRQ 的 G 數量。
gfreecnt:狀態為 Gdead 的數量,即 status = 6。
timerslen:timer 的數量。
M
p:綁定的 P 編號。
curg:當前正在 M 上執行代碼的 G 編號。
mallocing:是否正存在分配內存操作。
throwing:是否有拋出異常。
preemptoff:如果 preemptoff != "",則保持 curg 在這個 M 上運行。
locks:M 的 locks 數量。
dying:M 的 dying 值,其存在 0、1、2 和其他值四種處理情況。
spinning:是否處于自選狀態。
blocked:是否處于阻塞狀態。
lockedg:與 G 的 lockedm 相對應,它們的類型是 uintptr,記錄不被垃圾收集器跟蹤的 M 與繞過寫屏障的 G。
G
status: 同 P 的狀態類似,其詳細分類與描述同樣可查看源碼 runtime/runtime2.go 的代碼;if status==Gwaiting ,即 status 的值為 4 時,其括號內還會輸出具體的等待原因。
m:綁定的 M 編號,如果其值為 -1,代表無綁定。
lockedm:與 M 中的 lockedg 對應。
可視化調度快照
明白了上述各項指標的含義之后。為了繪圖簡單,我們將 GOMAXPROCS 設定為2,并選取第 1 秒的輸出內容進行可視化分析。
$?GOMAXPROCS=2?GODEBUG=schedtrace=1000,scheddetail=1?./demo ... SCHED?1004ms:?gomaxprocs=2?idleprocs=0?threads=4?spinningthreads=0?idlethreads=1?runqueue=10?gcwaiting=0?nmidlelocked=0?stopwait=0?sysmonwait=0P0:?status=1?schedtick=45?syscalltick=0?m=3?runqsize=4?gfreecnt=0?timerslen=0P1:?status=1?schedtick=48?syscalltick=0?m=0?runqsize=4?gfreecnt=0?timerslen=0M3:?p=0?curg=24?mallocing=0?throwing=0?preemptoff=?locks=0?dying=0?spinning=false?blocked=false?lockedg=-1M2:?p=-1?curg=-1?mallocing=0?throwing=0?preemptoff=?locks=0?dying=0?spinning=false?blocked=true?lockedg=-1M1:?p=-1?curg=-1?mallocing=0?throwing=0?preemptoff=?locks=2?dying=0?spinning=false?blocked=false?lockedg=-1M0:?p=1?curg=34?mallocing=0?throwing=0?preemptoff=?locks=0?dying=0?spinning=false?blocked=false?lockedg=-1G1:?status=4(semacquire)?m=-1?lockedm=-1G2:?status=4(force?gc?(idle))?m=-1?lockedm=-1G3:?status=4(GC?sweep?wait)?m=-1?lockedm=-1G4:?status=4(GC?scavenge?wait)?m=-1?lockedm=-1G17:?status=1()?m=-1?lockedm=-1G18:?status=1()?m=-1?lockedm=-1G19:?status=1()?m=-1?lockedm=-1G20:?status=1()?m=-1?lockedm=-1G21:?status=1()?m=-1?lockedm=-1G22:?status=1()?m=-1?lockedm=-1G23:?status=1()?m=-1?lockedm=-1G24:?status=2()?m=3?lockedm=-1G25:?status=1()?m=-1?lockedm=-1G26:?status=1()?m=-1?lockedm=-1G27:?status=1()?m=-1?lockedm=-1G28:?status=1()?m=-1?lockedm=-1G29:?status=1()?m=-1?lockedm=-1G30:?status=1()?m=-1?lockedm=-1G31:?status=1()?m=-1?lockedm=-1G32:?status=1()?m=-1?lockedm=-1G33:?status=1()?m=-1?lockedm=-1G34:?status=2()?m=0?lockedm=-1G35:?status=1()?m=-1?lockedm=-1G36:?status=1()?m=-1?lockedm=-1 ...該時刻的調度情況快照圖示如下
我們可以根據詳細信息獲取到 P、M、G 的具體運行情況。但需要注意的是,有一點我們不能確定,就是各個 P 的 LRQ 與 GRQ 是哪些具體的 G 在隊列中等待,但這并不妨礙大局(因此,上圖中的 LRQ 和 GRQ 可能并不是實際的 G 編號)。
總結
本文介紹了通過增加環境變量 GODEBUG ,我們可以在不做任何代碼改變或增加額外的插件情況下,方便地查看 Go 程序的調度情況。
讀者若想更直觀地理解 Go 語言的 GMP 和調度系統,不妨一試 。
機器鈴砍菜刀
歡迎添加小菜刀微信
加入Golang分享群學習交流!
感謝你的點贊和在看哦~
總結
以上是生活随笔為你收集整理的如何更直观地理解 Go 调度过程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 边城小猿——某二线城程序员15年的工作经
- 下一篇: 基于单片机智能婴儿车控制设计(毕业设计)