Go調度器系列(2)宏觀看調度器

上一篇文章《Go語言高階:調度器系列(1)起源》,學goroutine調度器以前的一些背景知識,這篇文章則是爲了對調度器有個宏觀的認識,從宏觀的3個角度,去看待和理解調度器是什麼樣子的,但仍然不涉及具體的調度原理git

三個角度分別是:github

  1. 調度器的宏觀組成
  2. 調度器的生命週期
  3. GMP的可視化感覺

在開始前,先回憶下調度器相關的3個縮寫:golang

  • G: goroutine,每一個G都表明1個goroutine
  • M: 工做線程,是Go語言定義出來在用戶層面描述系統線程的對象 ,每一個M表明一個系統線程
  • P: 處理器,它包含了運行Go代碼的資源。

3者的簡要關係是P擁有G,M必須和一個P關聯才能運行P擁有的G。數組

調度器的功能

《Go語言高階:調度器系列(1)起源》中介紹了協程和線程的關係,協程須要運行在線程之上,線程由CPU進行調度。bash

在Go中,線程是運行goroutine的實體,調度器的功能是把可運行的goroutine分配到工做線程上less

Go的調度器也是通過了多個版本的開發纔是如今這個樣子的,函數

  • 1.0版本發佈了最初的、最簡單的調度器,是G-M模型,存在4類問題
  • 1.1版本從新設計,修改成G-P-M模型,奠基當前調度器基本模樣
  • 1.2版本加入了搶佔式調度,防止協程不讓出CPU致使其餘G餓死
$GOROOT/src/runtime/proc.go的開頭註釋中包含了對Scheduler的重要註釋,介紹Scheduler的設計曾拒絕過3種方案以及緣由,本文再也不介紹了,但願你不要忽略爲數很少的官方介紹。

Scheduler的宏觀組成

Tony Bai《也談goroutine調度器》中的這幅圖,展現了goroutine調度器和系統調度器的關係,而不是把兩者割裂開來,而且從宏觀的角度展現了調度器的重要組成。測試

自頂向下是調度器的4個部分:ui

  1. 全局隊列(Global Queue):存放等待運行的G。
  2. P的本地隊列:同全局隊列相似,存放的也是等待運行的G,存的數量有限,不超過256個。新建G'時,G'優先加入到P的本地隊列,若是隊列滿了,則會把本地隊列中一半的G移動到全局隊列。
  3. P列表:全部的P都在程序啓動時建立,並保存在數組中,最多有GOMAXPROCS個。
  4. M:線程想運行任務就得獲取P,從P的本地隊列獲取G,P隊列爲空時,M也會嘗試從全局隊列一批G放到P的本地隊列,或從其餘P的本地隊列一半放到本身P的本地隊列。M運行G,G執行以後,M會從P獲取下一個G,不斷重複下去。

Goroutine調度器和OS調度器是經過M結合起來的,每一個M都表明了1個內核線程,OS調度器負責把內核線程分配到CPU的核上執行spa

調度器的生命週期

接下來咱們從另一個宏觀角度——生命週期,認識調度器。

全部的Go程序運行都會通過一個完整的調度器生命週期:從建立到結束。

即便下面這段簡單的代碼:

package main

import "fmt"

// main.main
func main() {
    fmt.Println("Hello scheduler")
}

也會經歷如上圖所示的過程:

  1. runtime建立最初的線程m0和goroutine g0,並把2者關聯。
  2. 調度器初始化:初始化m0、棧、垃圾回收,以及建立和初始化由GOMAXPROCS個P構成的P列表。
  3. 示例代碼中的main函數是main.mainruntime中也有1個main函數——runtime.main,代碼通過編譯後,runtime.main會調用main.main,程序啓動時會爲runtime.main建立goroutine,稱它爲main goroutine吧,而後把main goroutine加入到P的本地隊列。
  4. 啓動m0,m0已經綁定了P,會從P的本地隊列獲取G,獲取到main goroutine。
  5. G擁有棧,M根據G中的棧信息和調度信息設置運行環境
  6. M運行G
  7. G退出,再次回到M獲取可運行的G,這樣重複下去,直到main.main退出,runtime.main執行Defer和Panic處理,或調用runtime.exit退出程序。

調度器的生命週期幾乎佔滿了一個Go程序的一輩子,runtime.main的goroutine執行以前都是爲調度器作準備工做,runtime.main的goroutine運行,纔是調度器的真正開始,直到runtime.main結束而結束。

GMP的可視化感覺

上面的兩個宏觀角度,都是根據文檔、代碼整理出來,最後咱們從可視化角度感覺下調度器,有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

效果:

trace1

從上至下分別是goroutine(G)、堆、線程(M)、Proc(P)的信息,從左到右是時間線。用鼠標點擊顏色塊,最下面會列出詳細的信息。

咱們能夠發現:

  • runtime.main的goroutine是g1,這個編號應該永遠都不變的,runtime.main是在g0以後建立的第一個goroutine。
  • g1中調用了main.main,建立了trace goroutine g18。g1運行在P2上,g18運行在P0上。
  • P1上實際上也有goroutine運行,能夠看到短暫的豎線。

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

看到這密密麻麻的文字就有點擔憂,不要愁!由於每行字段都是同樣的,各字段含義以下:

  • SCHED:調試信息輸出標誌字符串,表明本行是goroutine調度器的輸出;
  • 0ms:即從程序啓動到輸出這行日誌的時間;
  • gomaxprocs: P的數量,本例有8個P;
  • idleprocs: 處於idle狀態的P的數量;經過gomaxprocs和idleprocs的差值,咱們就可知道執行go代碼的P的數量;
  • threads: os threads/M的數量,包含scheduler使用的m數量,加上runtime自用的相似sysmon這樣的thread的數量;
  • spinningthreads: 處於自旋狀態的os thread數量;
  • idlethread: 處於idle狀態的os thread的數量;
  • runqueue=0: Scheduler全局隊列中G的數量;
  • [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,主要看圈黃的地方:

  • 第1處:P1和M2進行了綁定。
  • 第2處:M2和P1進行了綁定,但M2上沒有運行的G。
  • 第3處:代碼中使用fmt進行打印,會進行系統調用,P1系統調用的次數不少,說明咱們的用例函數基本在P1上運行。
  • 第4處和第5處:M0上運行了G1,G1的狀態爲3(系統調用),G進行系統調用時,M會和P解綁,但M會記住以前的P,因此M0仍然記綁定了P1,而P1稱未綁定M。

總結時刻

這篇文章,從3個宏觀的角度介紹了調度器,也許你依然不知道調度器的原理,內心感受模模糊糊,不要緊,一步一步走,經過這篇文章但願你瞭解了:

  1. Go調度器和OS調度器的關係
  2. Go調度器的生命週期/整體流程
  3. P的數量等於GOMAXPROCS
  4. M須要經過綁定的P獲取G,而後執行G,不斷重複這個過程

示例代碼

本文全部示例代碼都在Github,可經過閱讀原文訪問:golang_step_by_step/tree/master/scheduler

參考資料

最近的感覺是:本身懂是一個層次,能寫出來須要擡升一個層次,給他人講懂又須要擡升一個層次。但願朋友們有所收穫。

  1. 若是這篇文章對你有幫助,請點個贊/喜歡,感謝
  2. 本文做者:大彬
  3. 若是喜歡本文,隨意轉載,但請保留此原文連接:http://lessisbetter.site/2019/03/26/golang-scheduler-2-macro-view/

相關文章
相關標籤/搜索