上一篇文章《Go語(yǔ)言高階:調(diào)度器系列(1)起源》,學(xué)goroutine調(diào)度器之前的一些背景知識(shí)痰驱,這篇文章則是為了對(duì)調(diào)度器有個(gè)宏觀的認(rèn)識(shí)既鞠,從宏觀的3個(gè)角度,去看待和理解調(diào)度器是什么樣子的朦肘,但仍然不涉及具體的調(diào)度原理饭弓。
三個(gè)角度分別是:
- 調(diào)度器的宏觀組成
- 調(diào)度器的生命周期
- GMP的可視化感受
在開(kāi)始前,先回憶下調(diào)度器相關(guān)的3個(gè)縮寫:
- G: goroutine媒抠,每個(gè)G都代表1個(gè)goroutine
- M: 工作線程弟断,是Go語(yǔ)言定義出來(lái)在用戶層面描述系統(tǒng)線程的對(duì)象 ,每個(gè)M代表一個(gè)系統(tǒng)線程
- P: 處理器趴生,它包含了運(yùn)行Go代碼的資源阀趴。
3者的簡(jiǎn)要關(guān)系是P擁有G,M必須和一個(gè)P關(guān)聯(lián)才能運(yùn)行P擁有的G冲秽。
調(diào)度器的功能
《Go語(yǔ)言高階:調(diào)度器系列(1)起源》中介紹了協(xié)程和線程的關(guān)系舍咖,協(xié)程需要運(yùn)行在線程之上,線程由CPU進(jìn)行調(diào)度锉桑。
在Go中排霉,線程是運(yùn)行g(shù)oroutine的實(shí)體,調(diào)度器的功能是把可運(yùn)行的goroutine分配到工作線程上民轴。
Go的調(diào)度器也是經(jīng)過(guò)了多個(gè)版本的開(kāi)發(fā)才是現(xiàn)在這個(gè)樣子的攻柠,
- 1.0版本發(fā)布了最初的、最簡(jiǎn)單的調(diào)度器后裸,是G-M模型瑰钮,存在4類問(wèn)題
- 1.1版本重新設(shè)計(jì),修改為G-P-M模型微驶,奠定當(dāng)前調(diào)度器基本模樣
- 1.2版本加入了搶占式調(diào)度浪谴,防止協(xié)程不讓出CPU導(dǎo)致其他G餓死
在
$GOROOT/src/runtime/proc.go
的開(kāi)頭注釋中包含了對(duì)Scheduler的重要注釋开睡,介紹Scheduler的設(shè)計(jì)曾拒絕過(guò)3種方案以及原因,本文不再介紹了苟耻,希望你不要忽略為數(shù)不多的官方介紹篇恒。
Scheduler的宏觀組成
Tony Bai在《也談goroutine調(diào)度器》中的這幅圖,展示了goroutine調(diào)度器和系統(tǒng)調(diào)度器的關(guān)系凶杖,而不是把二者割裂開(kāi)來(lái)胁艰,并且從宏觀的角度展示了調(diào)度器的重要組成。
自頂向下是調(diào)度器的4個(gè)部分:
- 全局隊(duì)列(Global Queue):存放等待運(yùn)行的G智蝠。
- P的本地隊(duì)列:同全局隊(duì)列類似腾么,存放的也是等待運(yùn)行的G,存的數(shù)量有限杈湾,不超過(guò)256個(gè)解虱。新建G'時(shí),G'優(yōu)先加入到P的本地隊(duì)列毛秘,如果隊(duì)列滿了饭寺,則會(huì)把本地隊(duì)列中一半的G移動(dòng)到全局隊(duì)列。
- P列表:所有的P都在程序啟動(dòng)時(shí)創(chuàng)建叫挟,并保存在數(shù)組中艰匙,最多有GOMAXPROCS個(gè)。
- M:線程想運(yùn)行任務(wù)就得獲取P抹恳,從P的本地隊(duì)列獲取G员凝,P隊(duì)列為空時(shí),M也會(huì)嘗試從全局隊(duì)列拿一批G放到P的本地隊(duì)列奋献,或從其他P的本地隊(duì)列偷一半放到自己P的本地隊(duì)列健霹。M運(yùn)行G,G執(zhí)行之后瓶蚂,M會(huì)從P獲取下一個(gè)G糖埋,不斷重復(fù)下去。
Goroutine調(diào)度器和OS調(diào)度器是通過(guò)M結(jié)合起來(lái)的窃这,每個(gè)M都代表了1個(gè)內(nèi)核線程瞳别,OS調(diào)度器負(fù)責(zé)把內(nèi)核線程分配到CPU的核上執(zhí)行。
調(diào)度器的生命周期
接下來(lái)我們從另外一個(gè)宏觀角度——生命周期杭攻,認(rèn)識(shí)調(diào)度器祟敛。
所有的Go程序運(yùn)行都會(huì)經(jīng)過(guò)一個(gè)完整的調(diào)度器生命周期:從創(chuàng)建到結(jié)束。
即使下面這段簡(jiǎn)單的代碼:
package main
import "fmt"
// main.main
func main() {
fmt.Println("Hello scheduler")
}
也會(huì)經(jīng)歷如上圖所示的過(guò)程:
- runtime創(chuàng)建最初的線程m0和goroutine g0兆解,并把2者關(guān)聯(lián)馆铁。
- 調(diào)度器初始化:初始化m0、棧锅睛、垃圾回收埠巨,以及創(chuàng)建和初始化由GOMAXPROCS個(gè)P構(gòu)成的P列表历谍。
- 示例代碼中的main函數(shù)是
main.main
,runtime
中也有1個(gè)main函數(shù)——runtime.main
辣垒,代碼經(jīng)過(guò)編譯后扮饶,runtime.main
會(huì)調(diào)用main.main
,程序啟動(dòng)時(shí)會(huì)為runtime.main
創(chuàng)建goroutine乍构,稱它為main goroutine吧,然后把main goroutine加入到P的本地隊(duì)列扛点。 - 啟動(dòng)m0哥遮,m0已經(jīng)綁定了P,會(huì)從P的本地隊(duì)列獲取G陵究,獲取到main goroutine眠饮。
- G擁有棧,M根據(jù)G中的棧信息和調(diào)度信息設(shè)置運(yùn)行環(huán)境
- M運(yùn)行G
- G退出铜邮,再次回到M獲取可運(yùn)行的G仪召,這樣重復(fù)下去,直到
main.main
退出松蒜,runtime.main
執(zhí)行Defer和Panic處理扔茅,或調(diào)用runtime.exit
退出程序。
調(diào)度器的生命周期幾乎占滿了一個(gè)Go程序的一生秸苗,runtime.main
的goroutine執(zhí)行之前都是為調(diào)度器做準(zhǔn)備工作召娜,runtime.main
的goroutine運(yùn)行,才是調(diào)度器的真正開(kāi)始惊楼,直到runtime.main
結(jié)束而結(jié)束玖瘸。
GMP的可視化感受
上面的兩個(gè)宏觀角度,都是根據(jù)文檔檀咙、代碼整理出來(lái)雅倒,最后我們從可視化角度感受下調(diào)度器,有2種方式弧可。
方式1:go tool trace
trace記錄了運(yùn)行時(shí)的信息蔑匣,能提供可視化的Web頁(yè)面。
簡(jiǎn)單測(cè)試代碼:main函數(shù)創(chuàng)建trace侣诺,trace會(huì)運(yùn)行在單獨(dú)的goroutine中殖演,然后main打印"Hello trace"退出。
func main() {
// 創(chuàng)建trace文件
f, err := os.Create("trace.out")
if err != nil {
panic(err)
}
defer f.Close()
// 啟動(dòng)trace goroutine
err = trace.Start(f)
if err != nil {
panic(err)
}
defer trace.Stop()
// main
fmt.Println("Hello trace")
}
運(yùn)行程序和運(yùn)行trace:
? trace git:(master) ? go run trace1.go
Hello trace
? trace git:(master) ? ls
trace.out trace1.go
? trace git:(master) ?
? trace git:(master) ? go tool trace trace.out
2019/03/24 20:48:22 Parsing trace...
2019/03/24 20:48:22 Splitting trace...
2019/03/24 20:48:22 Opening browser. Trace viewer is listening on http://127.0.0.1:55984
效果:
從上至下分別是goroutine(G)年鸳、堆趴久、線程(M)、Proc(P)的信息搔确,從左到右是時(shí)間線彼棍。用鼠標(biāo)點(diǎn)擊顏色塊灭忠,最下面會(huì)列出詳細(xì)的信息。
我們可以發(fā)現(xiàn):
-
runtime.main
的goroutine是g1
座硕,這個(gè)編號(hào)應(yīng)該永遠(yuǎn)都不變的弛作,runtime.main
是在g0
之后創(chuàng)建的第一個(gè)goroutine。 - g1中調(diào)用了
main.main
华匾,創(chuàng)建了trace goroutine g18
映琳。g1運(yùn)行在P2上,g18運(yùn)行在P0上蜘拉。 - P1上實(shí)際上也有g(shù)oroutine運(yùn)行萨西,可以看到短暫的豎線。
go tool trace的資料并不多旭旭,如果感興趣可閱讀:https://making.pusher.com/go-tool-trace/ 谎脯,中文翻譯是:https://mp.weixin.qq.com/s/nf_-AH_LeBN3913Pt6CzQQ 。
方式2:Debug trace
示例代碼:
// main.main
func main() {
for i := 0; i < 5; i++ {
time.Sleep(time.Second)
fmt.Println("Hello scheduler")
}
}
編譯和運(yùn)行持寄,運(yùn)行過(guò)程會(huì)打印trace:
? one_routine2 git:(master) ? go build .
? one_routine2 git:(master) ? GODEBUG=schedtrace=1000 ./one_routine2
結(jié)果:
SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=5 spinningthreads=1 idlethreads=0 runqueue=0 [0 0 0 0 0 0 0 0]
SCHED 1001ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
Hello scheduler
SCHED 2002ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
Hello scheduler
SCHED 3004ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
Hello scheduler
SCHED 4005ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
Hello scheduler
SCHED 5013ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
Hello scheduler
看到這密密麻麻的文字就有點(diǎn)擔(dān)心源梭,不要愁!因?yàn)槊啃凶侄味际且粯拥纳晕叮髯侄魏x如下:
- SCHED:調(diào)試信息輸出標(biāo)志字符串废麻,代表本行是goroutine調(diào)度器的輸出;
- 0ms:即從程序啟動(dòng)到輸出這行日志的時(shí)間仲闽;
- gomaxprocs: P的數(shù)量脑溢,本例有8個(gè)P;
- idleprocs: 處于idle狀態(tài)的P的數(shù)量赖欣;通過(guò)gomaxprocs和idleprocs的差值屑彻,我們就可知道執(zhí)行g(shù)o代碼的P的數(shù)量;
- threads: os threads/M的數(shù)量顶吮,包含scheduler使用的m數(shù)量社牲,加上runtime自用的類似sysmon這樣的thread的數(shù)量;
- spinningthreads: 處于自旋狀態(tài)的os thread數(shù)量悴了;
- idlethread: 處于idle狀態(tài)的os thread的數(shù)量搏恤;
- runqueue=0: Scheduler全局隊(duì)列中G的數(shù)量;
-
[0 0 0 0 0 0 0 0]
: 分別為8個(gè)P的local queue中的G的數(shù)量湃交。
看第一行熟空,含義是:剛啟動(dòng)時(shí)創(chuàng)建了8個(gè)P,其中5個(gè)空閑的P搞莺,共創(chuàng)建5個(gè)M息罗,其中1個(gè)M處于自旋,沒(méi)有M處于空閑才沧,8個(gè)P的本地隊(duì)列都沒(méi)有G迈喉。
再看個(gè)復(fù)雜版本的绍刮,加上scheddetail=1
可以打印更詳細(xì)的trace信息。
命令:
? one_routine2 git:(master) ? GODEBUG=schedtrace=1000,scheddetail=1 ./one_routine2
結(jié)果:
截圖可能更代碼匹配不起來(lái)挨摸,最初代碼是for死循環(huán)孩革,后面為了減少打印加了限制循環(huán)5次
每次分別打印了每個(gè)P毙芜、M躯概、G的信息淑际,P的數(shù)量等于gomaxprocs
探遵,M的數(shù)量等于threads
,主要看圈黃的地方:
- 第1處:P1和M2進(jìn)行了綁定邑茄。
- 第2處:M2和P1進(jìn)行了綁定簇秒,但M2上沒(méi)有運(yùn)行的G溯泣。
- 第3處:代碼中使用fmt進(jìn)行打印瞬女,會(huì)進(jìn)行系統(tǒng)調(diào)用,P1系統(tǒng)調(diào)用的次數(shù)很多努潘,說(shuō)明我們的用例函數(shù)基本在P1上運(yùn)行诽偷。
- 第4處和第5處:M0上運(yùn)行了G1,G1的狀態(tài)為3(系統(tǒng)調(diào)用)疯坤,G進(jìn)行系統(tǒng)調(diào)用時(shí)报慕,M會(huì)和P解綁,但M會(huì)記住之前的P压怠,所以M0仍然記綁定了P1眠冈,而P1稱未綁定M。
總結(jié)時(shí)刻
這篇文章菌瘫,從3個(gè)宏觀的角度介紹了調(diào)度器蜗顽,也許你依然不知道調(diào)度器的原理,心里感覺(jué)模模糊糊雨让,沒(méi)關(guān)系雇盖,一步一步走,通過(guò)這篇文章希望你了解了:
- Go調(diào)度器和OS調(diào)度器的關(guān)系
- Go調(diào)度器的生命周期/總體流程
- P的數(shù)量等于GOMAXPROCS
- M需要通過(guò)綁定的P獲取G栖忠,然后執(zhí)行G崔挖,不斷重復(fù)這個(gè)過(guò)程
示例代碼
本文所有示例代碼都在Github,可通過(guò)閱讀原文訪問(wèn):golang_step_by_step/tree/master/scheduler
參考資料
- Go程序的“一生”
- 也談goroutine調(diào)度器
- Debug trace, 當(dāng)前調(diào)度器設(shè)計(jì)人Dmitry Vyukov的文章
- Go tool trace中文翻譯
- Dave關(guān)于GODEBUG的介紹
最近的感受是:自己懂是一個(gè)層次庵寞,能寫出來(lái)需要抬升一個(gè)層次狸相,給他人講懂又需要抬升一個(gè)層次。希望朋友們有所收獲捐川。
- 如果這篇文章對(duì)你有幫助脓鹃,請(qǐng)點(diǎn)個(gè)贊/喜歡,感謝属拾。
- 本文作者:大彬
- 如果喜歡本文将谊,隨意轉(zhuǎn)載冷溶,但請(qǐng)保留此原文鏈接:http://lessisbetter.site/2019/03/26/golang-scheduler-2-macro-view/