Go排程器系列(3)圖解排程原理
如果你已經閱讀了前2篇文章:《排程起源》和《巨集觀看排程器》,你對G、P、M肯定已經不再陌生,我們這篇文章就介紹Go排程器的基本原理,本文總結了12個主要的場景,覆蓋了以下內容:
- G的建立和分配。
- P的本地佇列和全域性佇列的負載均衡。
- M如何尋找G。
- M如何從G1切換到G2。
- work stealing,M如何去偷G。
- 為何需要自旋執行緒。
- G進行系統呼叫,如何保證P的其他G'可以被執行,而不是餓死。
- Go排程器的搶佔。
12場景
提示:圖在前,場景描述在後。
上圖中三角形、正方形、圓形分別代表了M、P、G,正方形連線的綠色長方形代表了P的本地佇列。
場景1:p1擁有g1,m1獲取p1後開始執行g1,g1使用go func()
建立了g2,為了區域性性g2優先加入到p1的本地佇列。
場景2:g1執行完成後(函式:goexit
),m上執行的goroutine切換為g0,g0負責排程時協程的切換(函式:schedule
)。從p1的本地佇列取g2,從g0切換到g2,並開始執行g2(函式:execute
)。實現了執行緒m1的複用。
場景3:假設每個p的本地佇列只能存4個g。g2要建立了6個g,前4個g(g3, g4, g5, g6)已經加入p1的本地佇列,p1本地佇列滿了。
藍色長方形代表全域性佇列。
場景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,後續場景會有介紹)。
場景7:m2嘗試從全域性佇列(GQ)取一批g放到p2的本地佇列(函式:findrunnable
)。m2從全域性佇列取的g數量符合下面的公式:
n = min(len(GQ)/GOMAXPROCS + 1, len(GQ/2))
公式的含義是,至少從全域性佇列取1個g,但每次不要從全域性佇列移動太多的g到p本地佇列,給其他p留點。這是從全域性佇列到P本地佇列的負載均衡。
假定我們場景中一共有4個P,所以m2只從能從全域性佇列取1個g(即g3)移動p2本地佇列,然後完成從g0到g3的切換,執行g3。
場景8:假設g2一直在m1上執行,經過2輪後,m2已經把g7、g4也挪到了p2的本地佇列並完成執行,全域性佇列和p2的本地佇列都空了,如上圖左邊。
全域性佇列已經沒有g,那m就要執行work stealing:從其他有g的p哪裡偷取一半g過來,放到自己的P本地佇列。p2從p1的本地佇列尾部取一半的g,本例中一半則只有1個g8,放到p2的本地佇列,情況如上圖右邊。
場景9:p1本地佇列g5、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)巨集觀看排程器
參考資料
在學習排程器的時候,看了很多文章,這裡列一些重要的:
- The Go scheduler: https://morsmachine.dk/go-scheduler
- Go's work-stealing scheduler: https://rakyll.org/scheduler/,中文翻譯版: https://lingchao.xin/post/gos-work-stealing-scheduler.html
- Go夜讀:golang 中 goroutine 的排程: https://reading.developerlearning.cn/reading/12-2018-08-02-goroutine-gpm/
- Scheduling In Go : Part I、II、III: https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html,中文翻譯版: https://www.jianshu.com/p/cb6881a2661d
- 雨痕大神的golang原始碼剖析: github.com/qyuhen/book
- 也談goroutine排程器: https://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/
- kavya的排程PPT: https://speakerdeck.com/kavya719/the-scheduler-saga
- 搶佔的設計提案,Proposal: Non-cooperative goroutine preemption: https://github.com/golang/proposal/blob/master/design/24543-non-cooperative-preemption.md
相關文章
- Go排程器系列(2)巨集觀看排程器Go
- Go語言排程器之主動排程(20)Go
- Go runtime 排程器精講(五):排程策略Go
- Flink排程之排程器、排程策略、排程模式模式
- Go語言排程器之排程main goroutine(14)GoAI
- Go runtime 排程器精講(二):排程器初始化Go
- Go Runtime 的排程器Go
- [典藏版] Golang 排程器 GMP 原理與排程全分析Golang
- 圖解協程排程模型-GMP模型圖解模型
- Go排程器系列(4)原始碼閱讀與探索Go原始碼
- Go 排程模型 GPMGo模型
- 【深入理解Go】協程設計與排程原理(上)Go
- 【深入理解Go】協程設計與排程原理(下)Go
- Kubernetes 排程器
- 排程器簡介,以及Linux的排程策略Linux
- Pod的排程是由排程器(kube-scheduler)
- Yarn的排程器Yarn
- Go runtime 排程器精講(七):案例分析Go
- Go語言goroutine排程器初始化Go
- k8s排程器介紹(排程框架版本)K8S框架
- Go 的搶佔式排程Go
- Kubernetes叢集排程器原理剖析及思考
- 第3講:程序排程
- Go語言排程器之盜取goroutine(17)Go
- Go runtime 排程器精講(三):main goroutine 建立GoAI
- Go runtime 排程器精講(一):Go 程式初始化Go
- 也談goroutine排程器Go
- Linux I/O排程器Linux
- 深入 Java Timer 定時排程器實現原理Java
- GO GMP協程排程實現原理 5w字長文史上最全Go
- Go timer 是如何被排程的?Go
- kubernetes 排程
- 從原始碼分析 GMP 排程原理原始碼
- 定時排程系列之Quartz.Net詳解quartz
- Go 併發程式設計 - runtime 協程排程(三)Go程式設計
- Spark中資源排程和任務排程Spark
- Go runtime 排程器精講(十):非同步搶佔Go非同步
- Go runtime 排程器精講(四):執行 main goroutineGoAI