Go調度器介紹和容易忽視的問題

本文記錄了本人對Golang調度器的理解和跟蹤調度器的方法,特別是一個容易忽略的goroutine執行順序問題,看了不少篇Golang調度器的文章都沒提到這個點,分享出來一塊兒學習,歡迎交流指正。git

什麼是調度器

爲了方便剛接觸操做系統和高級語言的同窗,先用大白話介紹下什麼是調度器。 調度,是將多個程序合理的安排到有限的CPU上來使得每一個程序都可以得以執行,實現宏觀的併發執行。好比咱們的電腦CPU只有四核甚至雙核,但是咱們卻能夠在電腦上同時運行幾十個程序,這就是操做系統調度器的功勞。但操做系統調度的是進程和線程,線程簡單地說就是輕量級的進程,可是每一個線程仍須要MB級別的內存,並且若是兩個切換的線程在不一樣的進程中,還須要進程切換,會使CPU在調度這件事上花費大量時間。 爲了更合理的利用CPU,Golang經過goroutine原生支持高併發,goroutine是由go調度器在語言層面進行調度,將goroutine安排到線程上,能夠更充分地利用CPU。github

Golang的調度器

Golang的調度器在runtime中實現,咱們每一個運行的程序執行前都會運行一個runtime負責調度goroutine,咱們寫的代碼入口要在main包下的main函數中也是由於runtime.main函數會調用main.main。Golang的調度器在2012被重寫過一次,如今使用的是新版的G-P-M調度器,可是咱們仍是先來看下老的G-M調度器,這樣才能夠更好的體會當前調度器的強大之處。golang

G-M模型:

下面是舊調度器的G-P模型: shell

M:表明線程,goroutine都是由線程來執行的; Global G Queue:全局goroutine隊列,其中G就表明goroutine,全部M都從這個隊列中取出goroutine來執行。 這種模型比較簡單,可是問題也很明顯:

  1. 多個M訪問一個公共的全局G隊列,每次都須要加互斥鎖保護,形成激烈的鎖競爭和阻塞;
  2. 局部性不好,即若是M1上的G1建立了G2,須要將G2交給M2執行,但G1和G2是相關的,最好放在同一個M上執行。
  3. M中有mcache(內存分配狀態),消耗大量內存和較差的局部性。
  4. 系統調用syscall會阻塞線程,浪費不能合理的利用CPU。

G-P-M模型

後來Go語言開發者改善了調度器爲G-P-M模型,以下圖: markdown

其中G仍是表明goroutine,M表明線程,全局隊列依然存在;而新增長的P表明邏輯processor,如今G的眼中只有P,在G的眼裏P就是它的CPU。而且給每一個P新增長了局部隊列來保存本P要處理的goroutine。 這個模型的調度方法以下:

  1. 每一個P有個局部隊列,局部隊列保存待執行的goroutine
  2. 每一個P和一個M綁定,M是真正的執行P中goroutine的實體
  3. 正常狀況下,M從綁定的P中的局部隊列獲取G來執行
  4. 當M綁定的P的的局部隊列已經滿了以後就會把goroutine放到全局隊列
  5. M是複用的,不須要反覆銷燬和建立,擁有work stealing和hand off策略保證線程的高效利用。
  6. 當M綁定的P的局部隊列爲空時,M會從其餘P的局部隊列中偷取G來執行,即work stealing;當其餘P偷取不到G時,M會從全局隊列獲取到本地隊列來執行G。
  7. 當G因系統調用(syscall)阻塞時會阻塞M,此時P會和M解綁即hand off,並尋找新的idle的M,若沒有idle的M就會新建一個M。
  8. 當G因channel或者network I/O阻塞時,不會阻塞M,M會尋找其餘runnable的G;當阻塞的G恢復後會從新進入runnable進入P隊列等待執行
  9. mcache(內存分配狀態)位於P,因此G能夠跨M調度,再也不存在跨M調度局部性差的問題
  10. G是搶佔調度。不像操做系統按時間片調度線程那樣,Go調度器沒有時間片概念,G因阻塞和被搶佔而暫停,而且G只能在函數調用時有可能被搶佔,極端狀況下若是G一直作死循環就會霸佔一個P和M,Go調度器也無能爲力。

Go調度器奇怪的執行順序

是否是感受本身對Go調度器工做原理已經有個初步的瞭解了?下面指出一個坑給你踩一下,當心了! 請看下面這段代碼輸出什麼:併發

func main() {

	done := make(chan bool)

	values := []string{"a", "b", "c"}
	for _, v := range values {
		fmt.Println("--->", v)
		go func(u string) {
			fmt.Println(u)
			done <- true
		}(v)
	}

	// wait for all goroutines to complete before exiting
	for _ = range values {
		<-done
	}

}
複製代碼

先仔細想一下再看答案哦!函數

實際的數據結果是:高併發

---> a
---> b
---> c
c
b
a
複製代碼

Go調度器示例代碼能夠在跟着示例代碼學golang中查看,持續更新中,想系統學習Golang的同窗能夠關注一下。oop

可能你的第一反應是「不該該是輸出a,b,c,嗎?爲何輸出是c,a,b呢?」 這裏咱們雖然是使用for循環建立了3個goroutine,並且建立順序是a,b,c,按以前的分析應該是將a,b,c三個goroutine依次放進P的局部隊列,而後按照順序依次執行a,b,c所在的goroutine,爲何每次都是先執行c所在的goroutine呢?這是由於同一邏輯處理器中三個任務被建立後 理論上會按順序 被放在同一個任務隊列,但實際上最後那個任務會被放在專注的next(下一個要被執行的任務的意思)的位置,因此優先級最高,最可能先被執行,因此表現爲在同一個goroutine中建立的多個任務中最後建立那個任務最可能先被執行學習

這段解釋來自參考文章《Goroutine執行順序討論》中。

調度器狀態的查看方法

GODEBUG這個Go運行時環境變量非常強大,經過給其傳入不一樣的key1=value1,key2=value2… 組合,Go的runtime會輸出不一樣的調試信息,好比在這裏咱們給GODEBUG傳入了」schedtrace=1000″,其含義就是每1000ms,打印輸出一次goroutine scheduler的狀態。 下面演示使用Golang強大的GODEBUG環境變量能夠查看當前程序中Go調度器的狀態:

環境爲Windows10的Linux子系統(WSL),WSL搭建和使用的代碼在learn-golang項目有整理,代碼在文末參考的鳥窩的文章中也能夠找到。

func main() {
   var wg sync.WaitGroup
   wg.Add(10)
   for i := 0; i < 10; i++ {
   	go work(&wg)
   }
   wg.Wait()
   // Wait to see the global run queue deplete.
   time.Sleep(3 * time.Second)
}
func work(wg *sync.WaitGroup) {

   time.Sleep(time.Second)
   var counter int
   for i := 0; i < 1e10; i++ {
   	counter++
   }
   wg.Done()
}
複製代碼

編譯指令:

go build 01_GODEBUG-schedtrace.go
GODEBUG=schedtrace=1000 ./01_GODEBUG-schedtrace
複製代碼

結果:

SCHED 0ms: gomaxprocs=4 idleprocs=1 threads=5 spinningthreads=1 idlethreads=0 runqueue=0 [4 0 4 0]
SCHED 1000ms: gomaxprocs=4 idleprocs=4 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
SCHED 2007ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 3025ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 4033ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 5048ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 6079ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 7081ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 8092ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 9113ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 10129ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 11134ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 12157ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 13170ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 14183ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 15187ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 16187ms: gomaxprocs=4 idleprocs=2 threads=8 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0]
SCHED 17190ms: gomaxprocs=4 idleprocs=2 threads=8 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0]
SCHED 18193ms: gomaxprocs=4 idleprocs=2 threads=8 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0]
SCHED 19196ms: gomaxprocs=4 idleprocs=2 threads=8 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0]
SCHED 20200ms: gomaxprocs=4 idleprocs=4 threads=8 spinningthreads=0 idlethreads=6 runqueue=0 [0 0 0 0]
SCHED 21210ms: gomaxprocs=4 idleprocs=4 threads=8 spinningthreads=0 idlethreads=6 runqueue=0 [0 0 0 0]
SCHED 22219ms: gomaxprocs=4 idleprocs=4 threads=8 spinningthreads=0 idlethreads=6 runqueue=0 [0 0 0 0]
複製代碼

看到怎麼多輸出不要慌, 瞭解每一個字段的含義就很清晰了:

  • SCHED 1000ms 自程序運行開始經歷的時間
  • gomaxprocs=4 當前程序使用的邏輯processor,即P,小於等於CPU的核數。
  • idleprocs=4 空閒的線程數
  • threads=8 當前程序的總線程數M,包括在執行G的和空閒的
  • spinningthreads=0 處於自旋狀態的線程,即M在綁定的P的局部隊列和全局隊列都沒有G,M沒有銷燬而是在四處尋覓有沒有能夠steal的G,這樣能夠減小線程的大量建立。
  • idlethreads=3 處於idle空閒狀態的線程
  • runqueue=0 全局隊列中G的數目
  • [0 0 0 6] 本地隊列中的每一個P的局部隊列中G的數目,個人電腦是四核全部有四個P。

上面的輸出信息已經足夠咱們瞭解咱們的程序運行情況,要想看每一個goroutine、m和p的詳細調度信息,能夠在GODEBUG時加入,scheddetail

GODEBUG=schedtrace=1000,scheddetail=1 ./01_GODEBUG-schedtrace
複製代碼

結果以下:

SCHED 0ms: gomaxprocs=4 idleprocs=4 threads=7 spinningthreads=0 idlethreads=2 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
 P0: status=0 schedtick=7 syscalltick=1 m=-1 runqsize=0 gfreecnt=0
 P1: status=0 schedtick=2 syscalltick=1 m=-1 runqsize=0 gfreecnt=0
 P2: status=0 schedtick=1 syscalltick=1 m=-1 runqsize=0 gfreecnt=0
 P3: status=0 schedtick=1 syscalltick=1 m=-1 runqsize=0 gfreecnt=0
 M6: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
 M5: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
 M4: p=-1 curg=33 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
 M3: p=-1 curg=49 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
 M2: p=-1 curg=17 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
 M1: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=false blocked=false lockedg=-1
 M0: p=-1 curg=14 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
 G1: status=4(semacquire) m=-1 lockedm=-1
 G2: status=4(force gc (idle)) m=-1 lockedm=-1
 G3: status=4(GC sweep wait) m=-1 lockedm=-1
 G4: status=4(sleep) m=-1 lockedm=-1
 G5: status=4(sleep) m=-1 lockedm=-1
 G6: status=4(sleep) m=-1 lockedm=-1
 G7: status=4(sleep) m=-1 lockedm=-1
 G8: status=4(sleep) m=-1 lockedm=-1
 G9: status=4(sleep) m=-1 lockedm=-1
 G10: status=4(sleep) m=-1 lockedm=-1
 G11: status=4(sleep) m=-1 lockedm=-1
 G12: status=4(sleep) m=-1 lockedm=-1
 G13: status=4(sleep) m=-1 lockedm=-1
 G14: status=3() m=0 lockedm=-1
 G33: status=3() m=4 lockedm=-1
 G17: status=3() m=2 lockedm=-1
 G49: status=3() m=3 lockedm=-1
複製代碼

代碼能夠在跟着示例代碼學golang中查看,持續更新中,想系統學習Golang的同窗能夠關注一下。

參考資料:

大彬Go調度器系列

也談goroutine調度器

鳥窩 Go調度器跟蹤

Go調度器詳解

Goroutine執行順序討論

相關文章
相關標籤/搜索