如何更直观地理解 Go 调度过程
得益于 Go 語(yǔ)言優(yōu)秀的運(yùn)行時(shí)調(diào)度系統(tǒng),即使開(kāi)發(fā)人員沒(méi)有多線程編程經(jīng)驗(yàn),也能很容易地開(kāi)發(fā)并發(fā)程序。
調(diào)度系統(tǒng),其中最核心的就是 GMP 的設(shè)計(jì),欲深入理解 Go 語(yǔ)言設(shè)計(jì)的讀者都應(yīng)該看過(guò)這些知識(shí)。但是,在通過(guò)相關(guān)博客或者源碼學(xué)習(xí)時(shí),如果不能和實(shí)際的代碼進(jìn)行結(jié)合,在理解上或許不夠深刻。
本文介紹一種方式,即使用 GODEBUG 工具,通過(guò)實(shí)際運(yùn)行代碼來(lái)直觀地查看 Go 運(yùn)行時(shí)的調(diào)度過(guò)程。
調(diào)度簡(jiǎn)述
首先,我們先通過(guò)程序某時(shí)刻的調(diào)度快照示意圖,通過(guò)解析快照狀態(tài)來(lái)簡(jiǎn)單回顧一下 Go 調(diào)度系統(tǒng)。
如上圖所示,我們?cè)O(shè)定了 GOMAXPROCS=2,即 2 個(gè)處理器。
當(dāng)前時(shí)刻, P0 和 P1 上正分別掛載著 OS 線程 M1 與 M4,其上分別執(zhí)行著 G8 和 G17 的代碼。P0 的 LRQ(Local Run Queue,本地運(yùn)行隊(duì)列)有 3 個(gè) G 在排隊(duì)等待,而 P1 的 LRQ 已無(wú)等待的 G;GRQ (Global Run Queue,全局運(yùn)行隊(duì)列)中有 5 個(gè) G 。
網(wǎng)絡(luò)輪詢器 Net Poller 上有一個(gè)陷入異步網(wǎng)絡(luò)調(diào)用的 G9;M2 由于 G11 的某種同步系統(tǒng)調(diào)用而阻塞;M3 處于空閑狀態(tài),時(shí)刻準(zhǔn)備著當(dāng) M1 或 M4 被阻塞時(shí)而派上用場(chǎng)。
由于 P1 的 LRQ 已無(wú)等待的 G,當(dāng) G17 被調(diào)度時(shí),它將進(jìn)行 Wrok Stealing (任務(wù)竊取),其竊取源來(lái)自于其他處理器 P 的 LRQ(這里是 P1 的 LRQ)、GRQ 和 Net Poller,具體規(guī)則見(jiàn)runtime.schedule()函數(shù)。
GODEBUG 工具
啟用 GODEBUG 工具非常簡(jiǎn)單,只需要設(shè)置環(huán)境變量 GODEBUG 即可。它可以讓 Go 程序在運(yùn)行過(guò)程中輸出調(diào)試信息,能夠根據(jù)參數(shù)配置直觀地看到調(diào)度器或垃圾回收等詳細(xì)信息。
GODEBUG 的詳細(xì)描述介紹可見(jiàn)源碼runtime/extern.go文件。
本文我們關(guān)心的調(diào)試內(nèi)容是調(diào)度器,因此我們只使用 GODEBUG 的兩個(gè)參數(shù) schedtrace 與 scheddetail。
schedtrace=n:設(shè)置運(yùn)行時(shí)在每 n 毫秒輸出一行調(diào)度器的概要信息。
scheddetail: 輸出更詳細(xì)的調(diào)度信息。
示例代碼
我們使用的示例代碼如下
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() }代碼比較簡(jiǎn)單,我們啟動(dòng) 20 個(gè) CPU 密集型的 G 任務(wù),它們受到 WaitGroup 的限制。當(dāng)所有計(jì)算任務(wù)的 G 完成了各自的累加工作,程序才會(huì)結(jié)束執(zhí)行。
schedtrace 調(diào)度概要輸出
下面,我們?cè)O(shè)定 GODEBUG=schedtrace=1000,這意味著 1s 輸出一次程序的調(diào)度概要情況。
$?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:代表程序啟動(dòng)到輸出當(dāng)前行時(shí)的運(yùn)行時(shí)間,這個(gè)輸出間隔受到 schedtrace 值影響。
gomaxprocs:GOMAXPROCS 值,這里我們?cè)O(shè)定了其為 4。
idleprocs:空閑的 P 數(shù)量。
threads:運(yùn)行時(shí)管理的線程數(shù)。
spinningthreads:自旋線程,處于”自旋“狀態(tài)的線程數(shù)(避免頻繁的線程創(chuàng)建與銷(xiāo)毀)。
idlethreads:空閑線程數(shù)。
runqueue:全局隊(duì)列 GRQ 中的 G 數(shù)量。
[2 1 2 1]:代表 4 個(gè) P 的本地隊(duì)列 LRQ 中 G 數(shù)量分別是 2、1、2、1 。
scheddetail 調(diào)度詳細(xì)輸出
當(dāng)我們想要查看更詳細(xì)的調(diào)度信息時(shí),需要增加 scheddetail 參數(shù)。
$?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 ...當(dāng)增加了 scheddetail 參數(shù)后,其輸出信息不僅包含了 SCHED 的一行概覽信息,還增加了 GPM 三個(gè)實(shí)體狀況的詳細(xì)描述。
P
status:P 的運(yùn)行狀態(tài),其詳細(xì)分類(lèi)與描述可查看源碼 runtime/runtime2.go 的代碼。
schedtick:隨著每次調(diào)度行為累加,代表 P 的調(diào)度次數(shù)。
syscalltick:隨著每次系統(tǒng)調(diào)用行為累加,代表 P 的系統(tǒng)調(diào)用次數(shù)。
m: 綁定的 M 編號(hào),例如在 SCHED 1000ms 時(shí),P0 的 m=2,而 M2 的 p=0。
runqsize:LRQ 的 G 數(shù)量。
gfreecnt:狀態(tài)為 Gdead 的數(shù)量,即 status = 6。
timerslen:timer 的數(shù)量。
M
p:綁定的 P 編號(hào)。
curg:當(dāng)前正在 M 上執(zhí)行代碼的 G 編號(hào)。
mallocing:是否正存在分配內(nèi)存操作。
throwing:是否有拋出異常。
preemptoff:如果 preemptoff != "",則保持 curg 在這個(gè) M 上運(yùn)行。
locks:M 的 locks 數(shù)量。
dying:M 的 dying 值,其存在 0、1、2 和其他值四種處理情況。
spinning:是否處于自選狀態(tài)。
blocked:是否處于阻塞狀態(tài)。
lockedg:與 G 的 lockedm 相對(duì)應(yīng),它們的類(lèi)型是 uintptr,記錄不被垃圾收集器跟蹤的 M 與繞過(guò)寫(xiě)屏障的 G。
G
status: 同 P 的狀態(tài)類(lèi)似,其詳細(xì)分類(lèi)與描述同樣可查看源碼 runtime/runtime2.go 的代碼;if status==Gwaiting ,即 status 的值為 4 時(shí),其括號(hào)內(nèi)還會(huì)輸出具體的等待原因。
m:綁定的 M 編號(hào),如果其值為 -1,代表無(wú)綁定。
lockedm:與 M 中的 lockedg 對(duì)應(yīng)。
可視化調(diào)度快照
明白了上述各項(xiàng)指標(biāo)的含義之后。為了繪圖簡(jiǎn)單,我們將 GOMAXPROCS 設(shè)定為2,并選取第 1 秒的輸出內(nèi)容進(jìn)行可視化分析。
$?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 ...該時(shí)刻的調(diào)度情況快照?qǐng)D示如下
我們可以根據(jù)詳細(xì)信息獲取到 P、M、G 的具體運(yùn)行情況。但需要注意的是,有一點(diǎn)我們不能確定,就是各個(gè) P 的 LRQ 與 GRQ 是哪些具體的 G 在隊(duì)列中等待,但這并不妨礙大局(因此,上圖中的 LRQ 和 GRQ 可能并不是實(shí)際的 G 編號(hào))。
總結(jié)
本文介紹了通過(guò)增加環(huán)境變量 GODEBUG ,我們可以在不做任何代碼改變或增加額外的插件情況下,方便地查看 Go 程序的調(diào)度情況。
讀者若想更直觀地理解 Go 語(yǔ)言的 GMP 和調(diào)度系統(tǒng),不妨一試 。
機(jī)器鈴砍菜刀
歡迎添加小菜刀微信
加入Golang分享群學(xué)習(xí)交流!
感謝你的點(diǎn)贊和在看哦~
總結(jié)
以上是生活随笔為你收集整理的如何更直观地理解 Go 调度过程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 边城小猿——某二线城程序员15年的工作经
- 下一篇: 基于单片机智能婴儿车控制设计(毕业设计)