golang的goroutine調度機制
golang的goroutine調度機制
2016年09月26日 14:28:08 閱讀數:5664一直對goroutine的調度機制很好奇,最近在看雨痕的golang源碼分析,(基於go1.4)
感覺豁然開朗,受益匪淺;
去繁就簡,再加上自己的一些理解,整理了一下
~~
調度器
主要基於三個基本對象上,G,M,P(定義在源碼的src/runtime/runtime.h文件中)
1. G代表一個goroutine對象,每次go調用的時候,都會創建一個G對象
2. M代表一個線程,每次創建一個M的時候,都會有一個底層線程創建;所有的G任務,最終還是在M上執行
3. P代表一個處理器,每一個運行的M都必須綁定一個P,就像線程必須在麽一個CPU核上執行一樣
P的個數就是GOMAXPROCS(最大256),啟動時固定的,一般不修改; M的個數和P的個數不一定一樣多(會有休眠的M或者不需要太多的M)(最大10000);每一個P保存著本地G任務隊列,也有一個全局G任務隊列;
如下圖所示
全局G任務隊列會和各個本地G任務隊列按照一定的策略互相交換(滿了,則把本地隊列的一半送給全局隊列)
P是用一個全局數組(255)來保存的,並且維護著一個全局的P空閑鏈表
每次go調用的時候,都會:
1. 創建一個G對象,加入到本地隊列或者全局隊列
2. 如果還有空閑的P,則創建一個M
3. M會啟動一個底層線程,循環執行能找到的G任務
4. G任務的執行順序是,先從本地隊列找,本地沒有則從全局隊列找(一次性轉移(全局G個數/P個數)個,再去其它P中找(一次性轉移一半),
5. 以上的G任務執行是按照隊列順序(也就是go調用的順序)執行的。(這個地方是不是覺得很奇怪??)
對於上面的第2-3步,創建一個M,其過程:
1. 先找到一個空閑的P,如果沒有則直接返回,(哈哈,這個地方就保證了進程不會占用超過自己設定的cpu個數)
2. 調用系統api創建線程,不同的操作系統,調用不一樣,其實就是和c語言創建過程是一致的,(windows用的是CreateThread,linux用的是clone系統調用),(*^__^*)嘻嘻……
3. 然後創建的這個線程裏面才是真正做事的,循環執行G任務
那就會有個問題,如果一個系統調用或者G任務執行太長,他就會一直占用這個線程,由於本地隊列的G任務是順序執行的,其它G任務就會阻塞了,怎樣中止長任務的呢?(這個地方我找了好久~o(╯□╰)o)
這樣滴,啟動的時候,會專門創建一個線程sysmon,用來監控和管理,在內部是一個循環:
1. 記錄所有P的G任務計數schedtick,(schedtick會在每執行一個G任務後遞增)
2. 如果檢查到 schedtick一直沒有遞增,說明這個P一直在執行同一個G任務,如果超過一定的時間(10ms),就在這個G任務的棧信息裏面加一個標記
3. 然後這個G任務在執行的時候,如果遇到非內聯函數調用,就會檢查一次這個標記,然後中斷自己,把自己加到隊列末尾,執行下一個G
4. O(∩_∩)O哈哈~,如果沒有遇到非內聯函數(有時候正常的小函數會被優化成內聯函數)調用的話,那就慘了,會一直執行這個G任務,直到它自己結束;如果是個死循環,並且GOMAXPROCS=1的話,恭喜你,夯住了!親測,的確如此
對於一個G任務,中斷後的恢復過程:
1. 中斷的時候將寄存器裏的棧信息,保存到自己的G對象裏面
2. 當再次輪到自己執行時,將自己保存的棧信息復制到寄存器裏面,這樣就接著上次之後運行了。 ~\(≧▽≦)/~
但是還有一個問題,就是系統啟動的過程,雨痕沒有說的太明白,我一直有很多問題都狠疑惑(第一個M怎麽來的?,G怎麽找到對應的P?等等),這個讓我蛋疼了好久~
不過我自己意淫了一下,補充在下面,歡迎大家指正
1. 系統啟動的時候,首先跑的是主線程,那第一個M應該就是主線程吧(按照C語言的理解,嘿嘿),這裏叫M1,可以看前面的圖
2. 然後這個主線程會綁定第一個P1
3. 咱們寫的main函數,其實是作為一個goroutine來執行的(雨痕說的)
4. 也就是第一個P1就有了一個G1任務,然後第一個M1就執行這個G1任務(也就是main函數),創建這個G1的時候不用創建M了,因為已經有了M1
5. 這個main函數裏面所有的goroutine,都綁定到當前的M1所對應的P1上,O(∩_∩)O哈哈~
6. 然後創建main裏的goroutine的時候(比如G2),就會創建新的M2,新的M2裏的初始P2的本地任務隊列是空的,會從P1裏面取一些過來,哈哈
7. 這樣兩個M1,M2各自執行自己的G任務,再依次往復,這下就圓滿了~~~
綜上:
所以goroutine是按照搶占式調度的,一個goroutine最多執行10ms就會換作下一個
這個和目前主流系統的的cpu調度類似(按照時間分片)
windows:20ms
linux:5ms-800ms
到這裏都差不多了,這些在雨痕的筆記裏面都有更詳細的描述,不過很多地方比較淩亂,比較復雜,這裏篩檢了很多,方便讀者理解
註意:
1. 在Golang中編譯器也會嘗試進行內聯,將小函數直接復制並編譯,為了內聯,盡量消除編譯器無法偵測的dead code,利用gobuild -gcflags=-m編譯命令可以查看程序內聯狀態,不得不說golang的編譯工具鏈還是很強大的,十分有利於程序的優化。
如果有任何疑問,歡迎提出,
隨時更新
golang的goroutine調度機制