調度器分析
國內喜歡把Go的併發模型稱爲G-M-P模型,但在網上一查,貌似國外並無這樣的定義,他們喜歡直接稱其爲Go Scheduler——Go的調度器。無論如何,G-M-P都是Go調度器中的重要概念,它們都定義在sys/runtime/runtime2.go文件中,讓咱們看看它們都表明什麼吧:html
- G for Goroutine,定義於struct g,其存放着Goroutine的狀態信息,如保存着Goroutine的執行堆棧信息、Goroutine的等待信息和變量的GC信息等信息。咱們每用關鍵字go建立一個Goroutine,其在go程序的底層都建立了一個對應的G對象。
- M for Machine,定義於struct m,對應着操做系統的工做線程,其和物理處理器線程對應,它負責任務的調度,是實際驅動G運行的實體,但M不負責G的狀態的管理,須要切換執行的G時,M會把G的堆棧狀態寫回到G。實際上,在Go1.1以前,Go只有G-M模型,此前尚未P這個概念,直到Dmitry Vyukov在2012年發表了《Scalable Go Scheduler Design Doc》文章,改文章指出當時G-M模型的問題——用全局惟一的鎖和中心化的狀態來保護Goroutine相關的操做,Goroutine之間的切換可能會過載,每一個M之間的內存緩存問題,以及搶佔式線程的阻塞和非阻塞增長了不少開銷。所以提出引入Processors的概念到runtime中。
- P for Processor,定義於struct p,實現了邏輯上的處理器,它的責任是負責提供相關的上下文環境,負責內存緩存的管理,負責Goroutine任務隊列等。P是G和M的中間層,M會和P先綁定,而後M會不斷地從P的任務隊列中取出G並恢復執行(取出操做無鎖,由於沒有資源競爭的問題),當P的任務隊列都處理完,P再從全局隊列中返回一個G來執行(取出操做有鎖,由於這可能會和其它的P競爭),當全局隊列也沒有G時,則從其它的P竊取G來執行。當再也沒有G能夠被執行時,M和P會被解綁,進入休眠狀態。P的個數默認爲物理線程數。
Go的調度器纔是概念的重點,而G-M-P則是Go調度器組成的重要部分。P和M一般是對應的,簡單來講,P管理着一組G,並負責把G掛載到M上運行。當一個M長時間在運行同一個M時,runtime會建立一個新的M,阻塞的G所在的P會把其他的G掛載到新的M上,當這個阻塞的G阻塞完成或者結束時,該舊M會被回收。緩存
關於G的運行,由於M在運行G的過程當中,會遇到須要上下文切換的狀況——當一個被運行的G要被切換時,須要對G的執行現場進行保護,以便下次被調度執行時進行現場恢復,Go調度器的作法是,把M的堆棧和M所需的寄存器(SP、PC等)保存到G中,就能夠實現現場保護了。當調度器再次運行該G的時候,M經過訪問G中保存的寄存器進行現場恢復,便可從上次中斷的位置繼續執行。安全
Go這種調度器使用了m:n調度的技術,即複用或調度m個goroutine到n個OS線程。其中m的調度由Go程序的運行時負責,n的調度由OS負責。這讓m的調度能夠在用戶態下完成,不會形成內核態和用戶態見的頻繁切換。同時,內存的分配和釋放,文件的IO等,Go也經過內存池和netpoll等技術,儘可能減小內核態的調用。網絡
G的狀態
Goroutine在生命週期的不斷的階段,會有不一樣的G狀態。而經過分析G的狀態,有助於咱們瞭解Goroutine的調度。在runtime2.go文件中定義了,G有如下幾種狀態——idle, runnable, running, syscall, waiting, dead, copystack六種非GC狀態,以及scan, scanrunnable, scan running, scansyscall, scanwaiting六種對應的GC狀態,而moribund_unused和enqueue_unused兩種狀態已經被廢棄了:併發
- _Gidle for idle,意思是這個goroutine剛被建立出來,還未被進行初始化。由於它的值爲0,因此剛被建立出來的g對象都是_Gidle。但在runtime庫僅有的兩處調用中,建立出來的g都立刻被賦值爲_Gdead,這是爲了g在添加到被GC觀察以前,用於躲避trackbacks和stack scan,由於這個g對象在必要的處理前,還不是一個真正的goroutine。
- _Grunnable for runnable,意思是這個goroutine已經在運行隊列,在這種狀況下,goroutine還未執行用戶代碼,M的執行棧還不是goroutine本身的。
- _Grunning for running,意思是goroutine可能正在執行用戶代碼,M的執行棧已經由該goroutine所擁有,此時對象g不在運行隊列中。這個狀態值要待分配給M和P以後,交由M和P來設定。
- _Gsyscall for system scall,意思是這個goroutine正在執行系統調用,而不是正在執行用戶代碼。和_Grunning同樣,goroutine擁有了執行棧,也不在運行隊列中。這個狀態值只能由分配給的M來設定。
- _Gwaiting for waiting,意思是goroutine在運行時被阻塞,它既不執行用戶代碼,也不在運行隊列。它被記錄在其它的地方,例如管道等待隊列——channel wait queue,所以當須要該goroutine的時候,該goroutine能夠立刻就緒,這也是goroutine和channel的底層實現方式。這個時候,執行棧不被該g對象所擁有,除非一個管道正在作讀或者寫執行棧裏面數據的操做。除了以上這類型的狀況,在一個goroutine進入_Gwaiting以後嘗試獲取其執行棧,都是不安全的。
- _Gdead for dead,意思是這個goroutine在當前不被使用,這種狀況多是goroutine剛被建立出來,或者已經執行完畢退出並被放到釋放列表中。當一個G執行完畢並正在退出時,和G被添加到釋放列表時,G和G的執行棧都是M所擁有的。
- _Gcopystack for copy stack,意思是這個goroutine的執行棧已經被移動,這個goroutine即不執行用戶代碼,也不在運行隊列。這種狀態是_Grunning的時候,出現了執行棧空間不足或者過大,須要擴容或者GC的狀況下發生,是進行執行棧擴容或者收縮時的中間狀態。
- _Gscan系列,用於標記正在被GC掃描的狀態,這些狀態是由_Gscan=0x1000再加上_GRunnable, _Grunning, _Gsyscall和_Gwaiting的枚舉值所產生的,這麼作的好處是直接經過簡單的運算便可知道被Scan以前的狀態。當被標記爲這系列的狀態時,這些goroutine都不會執行用戶代碼,而且它們的執行棧都是被作該GC的goroutine所擁有。不過_Gscanrunning狀態有點特別,這個標記是爲了阻止正在運行的goroutine切換成其它狀態,並告訴這個G本身掃描本身的堆棧。正是這種巧妙的方式,使得Go語言的GC十分高效。
從以上列舉的狀態能夠分析出,不管是處理waiting的業務,仍是處理GC,goroutine是高效的。但當要調用system call的時候則否則,低效的系統調用業務代碼,會影響Go應用的運行性能,幸虧Go語言中已經封裝了不少能代替用戶低效的系統調用的工具,例如網絡調用看似是系統調用,但Go實際上已經在底層封裝了netpoll,咱們應該儘可能使用這些庫來避免系統調用。不合理的設計致使頻繁copy stack和會致使頻繁GC的設計等設計,這些都是咱們須要注意的。工具
2020.02post
tou.hwang性能
延伸閱讀:google
- The Go scheduler
- Scheduling In Go : Part I - OS Scheduler
- Scheduling In Go : Part II - Go Scheduler
- Scheduling In Go : Part III - Concurrency
參考資料:操作系統
- Scalable Go Scheduler Design Doc
- Go 1.13.1 源代碼
雖然有參考如下文章,但我以爲裏面的概念和說法未必都是對的,因此我對其內容我做從新思考和甄別,再做參考。
- Go 調度模型
- Go 調度模型
- 深刻Golang調度器之GMP模型
- Go語言——goroutine併發模型