Go調度器系列(3)圖解調度原理

若是你已經閱讀了前2篇文章:《調度起源》《宏觀看調度器》,你對G、P、M確定已經再也不陌生,咱們這篇文章就介紹Go調度器的基本原理,本文總結了12個主要的場景,覆蓋瞭如下內容:html

  1. G的建立和分配。
  2. P的本地隊列和全局隊列的負載均衡。
  3. M如何尋找G。
  4. M如何從G1切換到G2。
  5. work stealing,M如何去偷G。
  6. 爲什麼須要自旋線程。
  7. G進行系統調用,如何保證P的其餘G'能夠被執行,而不是餓死。
  8. Go調度器的搶佔。

12場景

提示:圖在前,場景描述在後。

上圖中三角形、正方形、圓形分別表明了M、P、G,正方形鏈接的綠色長方形表明了P的本地隊列。

場景1:p1擁有g1,m1獲取p1後開始運行g1,g1使用go func()建立了g2,爲了局部性g2優先加入到p1的本地隊列。git

場景2g1運行完成後(函數:goexit),m上運行的goroutine切換爲g0,g0負責調度時協程的切換(函數:schedule。從p1的本地隊列取g2,從g0切換到g2,並開始運行g2(函數:execute)。實現了線程m1的複用github

場景3:假設每一個p的本地隊列只能存4個g。g2要建立了6個g,前4個g(g3, g4, g5, g6)已經加入p1的本地隊列,p1本地隊列滿了。golang

藍色長方形表明全局隊列。

場景4:g2在建立g7的時候,發現p1的本地隊列已滿,須要執行負載均衡,把p1中本地隊列中前一半的g,還有新建立的g轉移到全局隊列(實現中並不必定是新的g,若是g是g2以後就執行的,會被保存在本地隊列,利用某個老的g替換新g加入全局隊列),這些g被轉移到全局隊列時,會被打亂順序。因此g3,g4,g7被轉移到全局隊列。併發

場景5:g2建立g8時,p1的本地隊列未滿,因此g8會被加入到p1的本地隊列。負載均衡

場景6在建立g時,運行的g會嘗試喚醒其餘空閒的p和m執行。假定g2喚醒了m2,m2綁定了p2,並運行g0,但p2本地隊列沒有g,m2此時爲自旋線程(沒有G但爲運行狀態的線程,不斷尋找g,後續場景會有介紹)。less

場景7:m2嘗試從全局隊列(GQ)取一批g放到p2的本地隊列(函數:findrunnable)。m2從全局隊列取的g數量符合下面的公式:函數

n = min(len(GQ)/GOMAXPROCS + 1, len(GQ/2))

公式的含義是,至少從全局隊列取1個g,但每次不要從全局隊列移動太多的g到p本地隊列,給其餘p留點。這是從全局隊列到P本地隊列的負載均衡oop

假定咱們場景中一共有4個P,因此m2只從能從全局隊列取1個g(即g3)移動p2本地隊列,而後完成從g0到g3的切換,運行g3。源碼分析

場景8:假設g2一直在m1上運行,通過2輪後,m2已經把g七、g4也挪到了p2的本地隊列並完成運行,全局隊列和p2的本地隊列都空了,如上圖左邊。

全局隊列已經沒有g,那m就要執行work stealing:從其餘有g的p哪裏偷取一半g過來,放到本身的P本地隊列。p2從p1的本地隊列尾部取一半的g,本例中一半則只有1個g8,放到p2的本地隊列,狀況如上圖右邊。

場景9:p1本地隊列g五、g6已經被其餘m偷走並運行完成,當前m1和m2分別在運行g2和g8,m3和m4沒有goroutine能夠運行,m3和m4處於自旋狀態,它們不斷尋找goroutine。爲何要讓m3和m4自旋,自旋本質是在運行,線程在運行卻沒有執行g,就變成了浪費CPU?銷燬線程不是更好嗎?能夠節約CPU資源。建立和銷燬CPU都是浪費時間的,咱們但願當有新goroutine建立時,馬上能有m運行它,若是銷燬再新建就增長了時延,下降了效率。固然也考慮了過多的自旋線程是浪費CPU,因此係統中最多有GOMAXPROCS個自旋的線程,多餘的沒事作線程會讓他們休眠(見函數:notesleep())。

場景10:假定當前除了m3和m4爲自旋線程,還有m5和m6爲自旋線程,g8建立了g9,g8進行了阻塞的系統調用,m2和p2當即解綁,p2會執行如下判斷:若是p2本地隊列有g、全局隊列有g或有空閒的m,p2都會立馬喚醒1個m和它綁定,不然p2則會加入到空閒P列表,等待m來獲取可用的p。本場景中,p2本地隊列有g,能夠和其餘自旋線程m5綁定。

場景11:(無圖場景)g8建立了g9,假如g8進行了非阻塞系統調用(CGO會是這種方式,見cgocall()),m2和p2會解綁,但m2會記住p,而後g8和m2進入系統調用狀態。當g8和m2退出系統調用時,會嘗試獲取p2,若是沒法獲取,則獲取空閒的p,若是依然沒有,g8會被記爲可運行狀態,並加入到全局隊列。

場景12:(無圖場景)Go調度在go1.12實現了搶佔,應該更精確的稱爲請求式搶佔,那是由於go調度器的搶佔和OS的線程搶佔比起來很柔和,不暴力,不會說線程時間片到了,或者更高優先級的任務到了,執行搶佔調度。go的搶佔調度柔和到只給goroutine發送1個搶佔請求,至於goroutine什麼時候停下來,那就管不到了。搶佔請求須要知足2個條件中的1個:1)G進行系統調用超過20us,2)G運行超過10ms。調度器在啓動的時候會啓動一個單獨的線程sysmon,它負責全部的監控工做,其中1項就是搶佔,發現知足搶佔條件的G時,就發出搶佔請求。

場景融合

若是把上面全部的場景都融合起來,就能構成下面這幅圖了,它從總體的角度描述了Go調度器各部分的關係。圖的上半部分是G的建立、負債均衡和work stealing,下半部分是M不停尋找和執行G的迭代過程。

若是你看這幅圖還有些似懂非懂,建議趕忙開始看雨痕大神的Golang源碼剖析,章節:併發調度。

總結,Go調度器和OS調度器相比,是至關的輕量與簡單了,但它已經足以撐起goroutine的調度工做了,而且讓Go具備了原生(強大)併發的能力,這是偉大的。若是你記住的很少,你必定要記住這一點:Go調度本質是把大量的goroutine分配到少許線程上去執行,並利用多核並行,實現更強大的併發。

下集預告

下篇會是源碼層面的內容了,關於源碼分析的書籍、文章能夠先看起來了,先劇透一篇圖,但願閱讀下篇文章趕忙關注本公衆號。

推薦閱讀

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

參考資料

在學習調度器的時候,看了不少文章,這裏列一些重要的:

  1. The Go scheduler: https://morsmachine.dk/go-sch...
  2. Go's work-stealing scheduler: https://rakyll.org/scheduler/,中文翻譯版: https://lingchao.xin/post/gos...
  3. Go夜讀:golang 中 goroutine 的調度: https://reading.developerlear...
  4. Scheduling In Go : Part I、II、III: https://www.ardanlabs.com/blo...,中文翻譯版: https://www.jianshu.com/p/cb6...
  5. 雨痕大神的golang源碼剖析: github.com/qyuhen/book
  6. 也談goroutine調度器: https://tonybai.com/2017/06/2...
  7. kavya的調度PPT: https://speakerdeck.com/kavya...
  8. 搶佔的設計提案,Proposal: Non-cooperative goroutine preemption: https://github.com/golang/pro...
  1. 若是這篇文章對你有幫助,請點個贊/喜歡,感謝
  2. 本文做者:大彬
  3. 若是喜歡本文,隨意轉載,但請保留此原文連接:http://lessisbetter.site/2019/04/04/golang-scheduler-3-principle-with-graph/

相關文章
相關標籤/搜索