golang的goroutine调度机制
一直對(duì)goroutine的調(diào)度機(jī)制很好奇,最近在看雨痕的golang源碼分析,(基于go1.4)
感覺(jué)豁然開(kāi)朗,受益匪淺;
去繁就簡(jiǎn),再加上自己的一些理解,整理了一下
~~
調(diào)度器
主要基于三個(gè)基本對(duì)象上,G,M,P(定義在源碼的src/runtime/runtime.h文件中)
1.???? G代表一個(gè)goroutine對(duì)象,每次go調(diào)用的時(shí)候,都會(huì)創(chuàng)建一個(gè)G對(duì)象
2.???? M代表一個(gè)線程,每次創(chuàng)建一個(gè)M的時(shí)候,都會(huì)有一個(gè)底層線程創(chuàng)建;所有的G任務(wù),最終還是在M上執(zhí)行
3.???? P代表一個(gè)處理器,每一個(gè)運(yùn)行的M都必須綁定一個(gè)P,就像線程必須在么一個(gè)CPU核上執(zhí)行一樣
P的個(gè)數(shù)就是GOMAXPROCS(最大256),啟動(dòng)時(shí)固定的,一般不修改; M的個(gè)數(shù)和P的個(gè)數(shù)不一定一樣多(會(huì)有休眠的M或者不需要太多的M)(最大10000);每一個(gè)P保存著本地G任務(wù)隊(duì)列,也有一個(gè)全局G任務(wù)隊(duì)列;
如下圖所示
全局G任務(wù)隊(duì)列會(huì)和各個(gè)本地G任務(wù)隊(duì)列按照一定的策略互相交換(滿了,則把本地隊(duì)列的一半送給全局隊(duì)列)
P是用一個(gè)全局?jǐn)?shù)組(255)來(lái)保存的,并且維護(hù)著一個(gè)全局的P空閑鏈表
每次go調(diào)用的時(shí)候,都會(huì):
1.???? 創(chuàng)建一個(gè)G對(duì)象,加入到本地隊(duì)列或者全局隊(duì)列
2.???? 如果還有空閑的P,則創(chuàng)建一個(gè)M
3.???? M會(huì)啟動(dòng)一個(gè)底層線程,循環(huán)執(zhí)行能找到的G任務(wù)
4.???? G任務(wù)的執(zhí)行順序是,先從本地隊(duì)列找,本地沒(méi)有則從全局隊(duì)列找(一次性轉(zhuǎn)移(全局G個(gè)數(shù)/P個(gè)數(shù))個(gè),再去其它P中找(一次性轉(zhuǎn)移一半),
5.???? 以上的G任務(wù)執(zhí)行是按照隊(duì)列順序(也就是go調(diào)用的順序)執(zhí)行的。(這個(gè)地方是不是覺(jué)得很奇怪??)
對(duì)于上面的第2-3步,創(chuàng)建一個(gè)M,其過(guò)程:
1.???? 先找到一個(gè)空閑的P,如果沒(méi)有則直接返回,(哈哈,這個(gè)地方就保證了進(jìn)程不會(huì)占用超過(guò)自己設(shè)定的cpu個(gè)數(shù))
2.???? 調(diào)用系統(tǒng)api創(chuàng)建線程,不同的操作系統(tǒng),調(diào)用不一樣,其實(shí)就是和c語(yǔ)言創(chuàng)建過(guò)程是一致的,(windows用的是CreateThread,linux用的是clone系統(tǒng)調(diào)用),(*^__^*)嘻嘻……
3.???? 然后創(chuàng)建的這個(gè)線程里面才是真正做事的,循環(huán)執(zhí)行G任務(wù)
那就會(huì)有個(gè)問(wèn)題,如果一個(gè)系統(tǒng)調(diào)用或者G任務(wù)執(zhí)行太長(zhǎng),他就會(huì)一直占用這個(gè)線程,由于本地隊(duì)列的G任務(wù)是順序執(zhí)行的,其它G任務(wù)就會(huì)阻塞了,怎樣中止長(zhǎng)任務(wù)的呢?(這個(gè)地方我找了好久~o(╯□╰)o)
這樣滴,啟動(dòng)的時(shí)候,會(huì)專門創(chuàng)建一個(gè)線程sysmon,用來(lái)監(jiān)控和管理,在內(nèi)部是一個(gè)循環(huán):
1.???? 記錄所有P的G任務(wù)計(jì)數(shù)schedtick,(schedtick會(huì)在每執(zhí)行一個(gè)G任務(wù)后遞增)
2.???? 如果檢查到?schedtick一直沒(méi)有遞增,說(shuō)明這個(gè)P一直在執(zhí)行同一個(gè)G任務(wù),如果超過(guò)一定的時(shí)間(10ms),就在這個(gè)G任務(wù)的棧信息里面加一個(gè)標(biāo)記
3.???? 然后這個(gè)G任務(wù)在執(zhí)行的時(shí)候,如果遇到非內(nèi)聯(lián)函數(shù)調(diào)用,就會(huì)檢查一次這個(gè)標(biāo)記,然后中斷自己,把自己加到隊(duì)列末尾,執(zhí)行下一個(gè)G
4.???? O(∩_∩)O哈哈~,如果沒(méi)有遇到非內(nèi)聯(lián)函數(shù)(有時(shí)候正常的小函數(shù)會(huì)被優(yōu)化成內(nèi)聯(lián)函數(shù))調(diào)用的話,那就慘了,會(huì)一直執(zhí)行這個(gè)G任務(wù),直到它自己結(jié)束;如果是個(gè)死循環(huán),并且GOMAXPROCS=1的話,恭喜你,夯住了!親測(cè),的確如此
對(duì)于一個(gè)G任務(wù),中斷后的恢復(fù)過(guò)程:
1.???? 中斷的時(shí)候?qū)⒓拇嫫骼锏?/span>棧信息,保存到自己的G對(duì)象里面
2.???? 當(dāng)再次輪到自己執(zhí)行時(shí),將自己保存的棧信息復(fù)制到寄存器里面,這樣就接著上次之后運(yùn)行了。 ~\(≧▽≦)/~
?
但是還有一個(gè)問(wèn)題,就是系統(tǒng)啟動(dòng)的過(guò)程,雨痕沒(méi)有說(shuō)的太明白,我一直有很多問(wèn)題都狠疑惑(第一個(gè)M怎么來(lái)的?,G怎么找到對(duì)應(yīng)的P?等等),這個(gè)讓我蛋疼了好久~
不過(guò)我自己意淫了一下,補(bǔ)充在下面,歡迎大家指正
1.???? 系統(tǒng)啟動(dòng)的時(shí)候,首先跑的是主線程,那第一個(gè)M應(yīng)該就是主線程吧(按照C語(yǔ)言的理解,嘿嘿),這里叫M1,可以看前面的圖
2.???? 然后這個(gè)主線程會(huì)綁定第一個(gè)P1
3.???? 咱們寫(xiě)的main函數(shù),其實(shí)是作為一個(gè)goroutine來(lái)執(zhí)行的(雨痕說(shuō)的)
4.???? 也就是第一個(gè)P1就有了一個(gè)G1任務(wù),然后第一個(gè)M1就執(zhí)行這個(gè)G1任務(wù)(也就是main函數(shù)),創(chuàng)建這個(gè)G1的時(shí)候不用創(chuàng)建M了,因為已經(jīng)有了M1
5.???? 這個(gè)main函數(shù)里面所有的goroutine,都綁定到當(dāng)前的M1所對(duì)應(yīng)的P1上,O(∩_∩)O哈哈~
6.???? 然后創(chuàng)建main里的goroutine的時(shí)候(比如G2),就會(huì)創(chuàng)建新的M2,新的M2里的初始P2的本地任務(wù)隊(duì)列是空的,會(huì)從P1里面取一些過(guò)來(lái),哈哈
7.???? 這樣兩個(gè)M1,M2各自執(zhí)行自己的G任務(wù),再依次往復(fù),這下就圓滿了~~~
?
綜上:
所以goroutine是按照搶占式調(diào)度的,一個(gè)goroutine最多執(zhí)行10ms就會(huì)換作下一個(gè)
這個(gè)和目前主流系統(tǒng)的的cpu調(diào)度類似(按照時(shí)間分片)
windows:20ms
linux:5ms-800ms
到這里都差不多了,這些在雨痕的筆記里面都有更詳細(xì)的描述,不過(guò)很多地方比較凌亂,比較復(fù)雜,這里篩檢了很多,方便讀者理解
?
注意:
1.???? 在Golang中編譯器也會(huì)嘗試進(jìn)行內(nèi)聯(lián),將小函數(shù)直接復(fù)制并編譯,為了內(nèi)聯(lián),盡量消除編譯器無(wú)法偵測(cè)的dead code,利用gobuild -gcflags=-m編譯命令可以查看程序內(nèi)聯(lián)狀態(tài),不得不說(shuō)golang的編譯工具鏈還是很強(qiáng)大的,十分有利于程序的優(yōu)化。
?
如果有任何疑問(wèn),歡迎提出,
隨時(shí)更新
(這篇文章是去年整理的,記錄公司內(nèi)部wiki上~)
總結(jié)
以上是生活随笔為你收集整理的golang的goroutine调度机制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: System x3100常见问题解答
- 下一篇: 索尼爱立信滑盖机java_“功能机时代”