Go调度器系列(3)图解调度原理
如果你已經(jīng)閱讀了前2篇文章:《調(diào)度起源》和《宏觀看調(diào)度器》,你對G、P、M肯定已經(jīng)不再陌生,我們這篇文章就介紹Go調(diào)度器的基本原理,本文總結(jié)了12個(gè)主要的場景,覆蓋了以下內(nèi)容:
12場景
提示:圖在前,場景描述在后。
image上圖中三角形、正方形、圓形分別代表了M、P、G,正方形連接的綠色長方形代表了P的本地隊(duì)列。
場景1:p1擁有g(shù)1,m1獲取p1后開始運(yùn)行g(shù)1,g1使用go func()創(chuàng)建了g2,為了局部性g2優(yōu)先加入到p1的本地隊(duì)列。
image場景2:g1運(yùn)行完成后(函數(shù):goexit),m上運(yùn)行的goroutine切換為g0,g0負(fù)責(zé)調(diào)度時(shí)協(xié)程的切換(函數(shù):schedule)。從p1的本地隊(duì)列取g2,從g0切換到g2,并開始運(yùn)行g(shù)2(函數(shù):execute)。實(shí)現(xiàn)了線程m1的復(fù)用。
image場景3:假設(shè)每個(gè)p的本地隊(duì)列只能存4個(gè)g。g2要創(chuàng)建了6個(gè)g,前4個(gè)g(g3, g4, g5, g6)已經(jīng)加入p1的本地隊(duì)列,p1本地隊(duì)列滿了。
image藍(lán)色長方形代表全局隊(duì)列。
場景4:g2在創(chuàng)建g7的時(shí)候,發(fā)現(xiàn)p1的本地隊(duì)列已滿,需要執(zhí)行負(fù)載均衡,把p1中本地隊(duì)列中前一半的g,還有新創(chuàng)建的g轉(zhuǎn)移到全局隊(duì)列(實(shí)現(xiàn)中并不一定是新的g,如果g是g2之后就執(zhí)行的,會被保存在本地隊(duì)列,利用某個(gè)老的g替換新g加入全局隊(duì)列),這些g被轉(zhuǎn)移到全局隊(duì)列時(shí),會被打亂順序。所以g3,g4,g7被轉(zhuǎn)移到全局隊(duì)列。
image場景5:g2創(chuàng)建g8時(shí),p1的本地隊(duì)列未滿,所以g8會被加入到p1的本地隊(duì)列。
image場景6:在創(chuàng)建g時(shí),運(yùn)行的g會嘗試喚醒其他空閑的p和m執(zhí)行。假定g2喚醒了m2,m2綁定了p2,并運(yùn)行g(shù)0,但p2本地隊(duì)列沒有g(shù),m2此時(shí)為自旋線程(沒有G但為運(yùn)行狀態(tài)的線程,不斷尋找g,后續(xù)場景會有介紹)。
[站外圖片上傳中...(image-d0a5f1-1554527569260)]
場景7:m2嘗試從全局隊(duì)列(GQ)取一批g放到p2的本地隊(duì)列(函數(shù):findrunnable)。m2從全局隊(duì)列取的g數(shù)量符合下面的公式:
n = min(len(GQ)/GOMAXPROCS + 1, len(GQ/2))公式的含義是,至少從全局隊(duì)列取1個(gè)g,但每次不要從全局隊(duì)列移動太多的g到p本地隊(duì)列,給其他p留點(diǎn)。這是從全局隊(duì)列到P本地隊(duì)列的負(fù)載均衡。
假定我們場景中一共有4個(gè)P,所以m2只從能從全局隊(duì)列取1個(gè)g(即g3)移動p2本地隊(duì)列,然后完成從g0到g3的切換,運(yùn)行g(shù)3。
image場景8:假設(shè)g2一直在m1上運(yùn)行,經(jīng)過2輪后,m2已經(jīng)把g7、g4也挪到了p2的本地隊(duì)列并完成運(yùn)行,全局隊(duì)列和p2的本地隊(duì)列都空了,如上圖左邊。
全局隊(duì)列已經(jīng)沒有g(shù),那m就要執(zhí)行work stealing:從其他有g(shù)的p哪里偷取一半g過來,放到自己的P本地隊(duì)列。p2從p1的本地隊(duì)列尾部取一半的g,本例中一半則只有1個(gè)g8,放到p2的本地隊(duì)列,情況如上圖右邊。
image場景9:p1本地隊(duì)列g(shù)5、g6已經(jīng)被其他m偷走并運(yùn)行完成,當(dāng)前m1和m2分別在運(yùn)行g(shù)2和g8,m3和m4沒有g(shù)oroutine可以運(yùn)行,m3和m4處于自旋狀態(tài),它們不斷尋找goroutine。為什么要讓m3和m4自旋,自旋本質(zhì)是在運(yùn)行,線程在運(yùn)行卻沒有執(zhí)行g(shù),就變成了浪費(fèi)CPU?銷毀線程不是更好嗎?可以節(jié)約CPU資源。創(chuàng)建和銷毀CPU都是浪費(fèi)時(shí)間的,我們希望當(dāng)有新goroutine創(chuàng)建時(shí),立刻能有m運(yùn)行它,如果銷毀再新建就增加了時(shí)延,降低了效率。當(dāng)然也考慮了過多的自旋線程是浪費(fèi)CPU,所以系統(tǒng)中最多有GOMAXPROCS個(gè)自旋的線程,多余的沒事做線程會讓他們休眠(見函數(shù):notesleep())。
image場景10:假定當(dāng)前除了m3和m4為自旋線程,還有m5和m6為自旋線程,g8創(chuàng)建了g9,g8進(jìn)行了阻塞的系統(tǒng)調(diào)用,m2和p2立即解綁,p2會執(zhí)行以下判斷:如果p2本地隊(duì)列有g(shù)、全局隊(duì)列有g(shù)或有空閑的m,p2都會立馬喚醒1個(gè)m和它綁定,否則p2則會加入到空閑P列表,等待m來獲取可用的p。本場景中,p2本地隊(duì)列有g(shù),可以和其他自旋線程m5綁定。
場景11:(無圖場景)g8創(chuàng)建了g9,假如g8進(jìn)行了非阻塞系統(tǒng)調(diào)用(CGO會是這種方式,見cgocall()),m2和p2會解綁,但m2會記住p,然后g8和m2進(jìn)入系統(tǒng)調(diào)用狀態(tài)。當(dāng)g8和m2退出系統(tǒng)調(diào)用時(shí),會嘗試獲取p2,如果無法獲取,則獲取空閑的p,如果依然沒有,g8會被記為可運(yùn)行狀態(tài),并加入到全局隊(duì)列。
場景12:(無圖場景)Go調(diào)度在go1.12實(shí)現(xiàn)了搶占,應(yīng)該更精確的稱為請求式搶占,那是因?yàn)間o調(diào)度器的搶占和OS的線程搶占比起來很柔和,不暴力,不會說線程時(shí)間片到了,或者更高優(yōu)先級的任務(wù)到了,執(zhí)行搶占調(diào)度。go的搶占調(diào)度柔和到只給goroutine發(fā)送1個(gè)搶占請求,至于goroutine何時(shí)停下來,那就管不到了。搶占請求需要滿足2個(gè)條件中的1個(gè):1)G進(jìn)行系統(tǒng)調(diào)用超過20us,2)G運(yùn)行超過10ms。調(diào)度器在啟動的時(shí)候會啟動一個(gè)單獨(dú)的線程sysmon,它負(fù)責(zé)所有的監(jiān)控工作,其中1項(xiàng)就是搶占,發(fā)現(xiàn)滿足搶占條件的G時(shí),就發(fā)出搶占請求。
場景融合
如果把上面所有的場景都融合起來,就能構(gòu)成下面這幅圖了,它從整體的角度描述了Go調(diào)度器各部分的關(guān)系。圖的上半部分是G的創(chuàng)建、負(fù)債均衡和work stealing,下半部分是M不停尋找和執(zhí)行G的迭代過程。
如果你看這幅圖還有些似懂非懂,建議趕緊開始看雨痕大神的Golang源碼剖析,章節(jié):并發(fā)調(diào)度。
image總結(jié),Go調(diào)度器和OS調(diào)度器相比,是相當(dāng)?shù)妮p量與簡單了,但它已經(jīng)足以撐起goroutine的調(diào)度工作了,并且讓Go具有了原生(強(qiáng)大)并發(fā)的能力,這是偉大的。如果你記住的不多,你一定要記住這一點(diǎn):Go調(diào)度本質(zhì)是把大量的goroutine分配到少量線程上去執(zhí)行,并利用多核并行,實(shí)現(xiàn)更強(qiáng)大的并發(fā)。
下集預(yù)告
下篇會是源碼層面的內(nèi)容了,關(guān)于源碼分析的書籍、文章可以先看起來了,先劇透一篇圖,希望閱讀下篇文章趕緊關(guān)注本公眾號。
image推薦閱讀
Go調(diào)度器系列(1)起源
Go調(diào)度器系列(2)宏觀看調(diào)度器
參考資料
在學(xué)習(xí)調(diào)度器的時(shí)候,看了很多文章,這里列一些重要的:
總結(jié)
以上是生活随笔為你收集整理的Go调度器系列(3)图解调度原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【思考】AI太热门,博士也不好找工作了吗
- 下一篇: jq判断CheckBox是否被选中的常见