調(diào)度器
主要基于三個基本對象上恶阴,G,M豹障,P(定義在源碼的src/runtime/runtime.h文件中)
- G代表一個goroutine對象冯事,每次go調(diào)用的時候,都會創(chuàng)建一個G對象
- M代表一個線程沼填,每次創(chuàng)建一個M的時候桅咆,都會有一個底層線程創(chuàng)建;所有的G任務(wù)坞笙,最終還是在M上執(zhí)行
- P代表一個處理器岩饼,每一個運(yùn)行的M都必須綁定一個P,就像線程必須在么一個CPU核上執(zhí)行一樣
P的個數(shù)就是GOMAXPROCS(最大256)薛夜,啟動時固定的籍茧,一般不修改;
M的個數(shù)和P的個數(shù)不一定一樣多(會有休眠的M或者不需要太多的M)(最大10000)梯澜;每一個P保存著本地G任務(wù)隊列寞冯,也有一個全局G任務(wù)隊列;
如下圖所示
全局G任務(wù)隊列會和各個本地G任務(wù)隊列按照一定的策略互相交換(滿了,則把本地隊列的一半送給全局隊列)
P是用一個全局?jǐn)?shù)組(255)來保存的吮龄,并且維護(hù)著一個全局的P空閑鏈表
每次go調(diào)用的時候俭茧,都會:
- 創(chuàng)建一個G對象,加入到本地隊列或者全局隊列
- 如果還有空閑的P漓帚,則創(chuàng)建一個M
- M會啟動一個底層線程母债,循環(huán)執(zhí)行能找到的G任務(wù)
- G任務(wù)的執(zhí)行順序是,先從本地隊列找尝抖,本地沒有則從全局隊列找(一次性轉(zhuǎn)移(全局G個數(shù)/P個數(shù))個毡们,再去其它P中找(一次性轉(zhuǎn)移一半),
- 以上的G任務(wù)執(zhí)行是按照隊列順序(也就是go調(diào)用的順序)執(zhí)行的昧辽。(這個地方是不是覺得很奇怪衙熔??)
對于上面的第2-3步搅荞,創(chuàng)建一個M红氯,其過程:
- 先找到一個空閑的P,如果沒有則直接返回咕痛,(哈哈脖隶,這個地方就保證了進(jìn)程不會占用超過自己設(shè)定的cpu個數(shù))
- 調(diào)用系統(tǒng)api創(chuàng)建線程,不同的操作系統(tǒng)暇检,調(diào)用不一樣,其實就是和c語言創(chuàng)建過程是一致的婉称,(windows用的是CreateThread块仆,linux用的是clone系統(tǒng)調(diào)用)
- 然后創(chuàng)建的這個線程里面才是真正做事的,循環(huán)執(zhí)行G任務(wù)
那就會有個問題王暗,如果一個系統(tǒng)調(diào)用或者G任務(wù)執(zhí)行太長悔据,他就會一直占用這個線程,由于本地隊列的G任務(wù)是順序執(zhí)行的俗壹,其它G任務(wù)就會阻塞了科汗,怎樣中止長任務(wù)的呢?
這樣滴绷雏,啟動的時候头滔,會專門創(chuàng)建一個線程sysmon,用來監(jiān)控和管理涎显,在內(nèi)部是一個循環(huán):
- 記錄所有P的G任務(wù)計數(shù)schedtick坤检,(schedtick會在每執(zhí)行一個G任務(wù)后遞增)
- 如果檢查到 schedtick一直沒有遞增,說明這個P一直在執(zhí)行同一個G任務(wù)期吓,如果超過一定的時間(10ms)早歇,就在這個G任務(wù)的棧信息里面加一個標(biāo)記
- 然后這個G任務(wù)在執(zhí)行的時候,如果遇到非內(nèi)聯(lián)函數(shù)調(diào)用,就會檢查一次這個標(biāo)記箭跳,然后中斷自己晨另,把自己加到隊列末尾,執(zhí)行下一個G
- 如果沒有遇到非內(nèi)聯(lián)函數(shù)(有時候正常的小函數(shù)會被優(yōu)化成內(nèi)聯(lián)函數(shù))調(diào)用的話谱姓,那就慘了借尿,會一直執(zhí)行這個G任務(wù),直到它自己結(jié)束逝段;如果是個死循環(huán)垛玻,并且GOMAXPROCS=1的話,恭喜你奶躯,夯住了帚桩!親測,的確如此
對于一個G任務(wù)嘹黔,中斷后的恢復(fù)過程:
- 中斷的時候?qū)⒓拇嫫骼锏臈P畔⒄撕浚4娴阶约旱腉對象里面
- 當(dāng)再次輪到自己執(zhí)行時,將自己保存的棧信息復(fù)制到寄存器里面儡蔓,這樣就接著上次之后運(yùn)行了郭蕉。
但是還有一個問題,就是系統(tǒng)啟動的過程
- 系統(tǒng)啟動的時候喂江,首先跑的是主線程召锈,那第一個M應(yīng)該就是主線程吧(按照C語言的理解,嘿嘿)获询,這里叫M1涨岁,可以看前面的圖
- 然后這個主線程會綁定第一個P1
- 咱們寫的main函數(shù),其實是作為一個goroutine來執(zhí)行的
- 也就是第一個P1就有了一個G1任務(wù)吉嚣,然后第一個M1就執(zhí)行這個G1任務(wù)(也就是main函數(shù))梢薪,創(chuàng)建這個G1的時候不用創(chuàng)建M了,因為已經(jīng)有了M1
- 這個main函數(shù)里面所有的goroutine尝哆,都綁定到當(dāng)前的M1所對應(yīng)的P1上
- 然后創(chuàng)建main里的goroutine的時候(比如G2)秉撇,就會創(chuàng)建新的M2,新的M2里的初始P2的本地任務(wù)隊列是空的秋泄,會從P1里面取一些過來琐馆,哈哈
- 這樣兩個M1,M2各自執(zhí)行自己的G任務(wù)印衔,再依次往復(fù)啡捶,這下就圓滿了
綜上:
所以goroutine是按照搶占式調(diào)度的,一個goroutine最多執(zhí)行10ms就會換作下一個
這個和目前主流系統(tǒng)的的cpu調(diào)度類似(按照時間分片)
windows:20ms
linux:5ms-800ms
注意:
在Golang中編譯器也會嘗試進(jìn)行內(nèi)聯(lián)奸焙,將小函數(shù)直接復(fù)制并編譯瞎暑,為了內(nèi)聯(lián)彤敛,盡量消除編譯器無法偵測的dead code,利用gobuild -gcflags=-m編譯命令可以查看程序內(nèi)聯(lián)狀態(tài)了赌,不得不說golang的編譯工具鏈還是很強(qiáng)大的墨榄,十分有利于程序的優(yōu)化。
如果有任何疑問勿她,歡迎提出袄秩,
作者:正版兩只羊
原文:https://blog.csdn.net/liangzhiyang/article/details/52669851