上一篇文章《Go語言高階:調度器系列(1)起源》,學goroutine調度器以前的一些背景知識,這篇文章則是爲了對調度器有個宏觀的認識,從宏觀的3個角度,去看待和理解調度器是什麼樣子的,但仍然不涉及具體的調度原理。git
三個角度分別是:github
在開始前,先回憶下調度器相關的3個縮寫:golang
3者的簡要關係是P擁有G,M必須和一個P關聯才能運行P擁有的G。數組
《Go語言高階:調度器系列(1)起源》中介紹了協程和線程的關係,協程須要運行在線程之上,線程由CPU進行調度。bash
在Go中,線程是運行goroutine的實體,調度器的功能是把可運行的goroutine分配到工做線程上。less
Go的調度器也是通過了多個版本的開發纔是如今這個樣子的,函數
在
$GOROOT/src/runtime/proc.go
的開頭註釋中包含了對Scheduler的重要註釋,介紹Scheduler的設計曾拒絕過3種方案以及緣由,本文再也不介紹了,但願你不要忽略爲數很少的官方介紹。
Tony Bai在《也談goroutine調度器》中的這幅圖,展現了goroutine調度器和系統調度器的關係,而不是把兩者割裂開來,而且從宏觀的角度展現了調度器的重要組成。測試
自頂向下是調度器的4個部分:ui
Goroutine調度器和OS調度器是經過M結合起來的,每一個M都表明了1個內核線程,OS調度器負責把內核線程分配到CPU的核上執行。spa
接下來咱們從另一個宏觀角度——生命週期,認識調度器。
全部的Go程序運行都會通過一個完整的調度器生命週期:從建立到結束。
即便下面這段簡單的代碼:
package main import "fmt" // main.main func main() { fmt.Println("Hello scheduler") }
也會經歷如上圖所示的過程:
main.main
,runtime
中也有1個main函數——runtime.main
,代碼通過編譯後,runtime.main
會調用main.main
,程序啓動時會爲runtime.main
建立goroutine,稱它爲main goroutine吧,而後把main goroutine加入到P的本地隊列。main.main
退出,runtime.main
執行Defer和Panic處理,或調用runtime.exit
退出程序。調度器的生命週期幾乎佔滿了一個Go程序的一輩子,runtime.main
的goroutine執行以前都是爲調度器作準備工做,runtime.main
的goroutine運行,纔是調度器的真正開始,直到runtime.main
結束而結束。
上面的兩個宏觀角度,都是根據文檔、代碼整理出來,最後咱們從可視化角度感覺下調度器,有2種方式。
方式1:go tool trace
trace記錄了運行時的信息,能提供可視化的Web頁面。
簡單測試代碼:main函數建立trace,trace會運行在單獨的goroutine中,而後main打印"Hello trace"退出。
func main() { // 建立trace文件 f, err := os.Create("trace.out") if err != nil { panic(err) } defer f.Close() // 啓動trace goroutine err = trace.Start(f) if err != nil { panic(err) } defer trace.Stop() // main fmt.Println("Hello trace") }
運行程序和運行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)的信息,從左到右是時間線。用鼠標點擊顏色塊,最下面會列出詳細的信息。
咱們能夠發現:
runtime.main
的goroutine是g1
,這個編號應該永遠都不變的,runtime.main
是在g0
以後建立的第一個goroutine。main.main
,建立了trace goroutine g18
。g1運行在P2上,g18運行在P0上。go tool trace的資料並很少,若是感興趣可閱讀:https://making.pusher.com/go-... ,中文翻譯是:https://mp.weixin.qq.com/s/nf... 。
方式2:Debug trace
示例代碼:
// main.main func main() { for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Println("Hello scheduler") } }
編譯和運行,運行過程會打印trace:
➜ one_routine2 git:(master) ✗ go build . ➜ one_routine2 git:(master) ✗ GODEBUG=schedtrace=1000 ./one_routine2
結果:
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
看到這密密麻麻的文字就有點擔憂,不要愁!由於每行字段都是同樣的,各字段含義以下:
[0 0 0 0 0 0 0 0]
: 分別爲8個P的local queue中的G的數量。看第一行,含義是:剛啓動時建立了8個P,其中5個空閒的P,共建立5個M,其中1個M處於自旋,沒有M處於空閒,8個P的本地隊列都沒有G。
再看個複雜版本的,加上scheddetail=1
能夠打印更詳細的trace信息。
命令:
➜ one_routine2 git:(master) ✗ GODEBUG=schedtrace=1000,scheddetail=1 ./one_routine2
結果:
截圖可能更代碼匹配不起來,最初代碼是for死循環,後面爲了減小打印加了限制循環5次
每次分別打印了每一個P、M、G的信息,P的數量等於gomaxprocs
,M的數量等於threads
,主要看圈黃的地方:
這篇文章,從3個宏觀的角度介紹了調度器,也許你依然不知道調度器的原理,內心感受模模糊糊,不要緊,一步一步走,經過這篇文章但願你瞭解了:
本文全部示例代碼都在Github,可經過閱讀原文訪問:golang_step_by_step/tree/master/scheduler
最近的感覺是:本身懂是一個層次,能寫出來須要擡升一個層次,給他人講懂又須要擡升一個層次。但願朋友們有所收穫。
- 若是這篇文章對你有幫助,請點個贊/喜歡,感謝。
- 本文做者:大彬
- 若是喜歡本文,隨意轉載,但請保留此原文連接:http://lessisbetter.site/2019/03/26/golang-scheduler-2-macro-view/