這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
golang的goroutine調度機制
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
目錄(?)[-]
- 一直對goroutine的調度機制很好奇最近在看雨痕的golang源碼分析基於go14
- 這篇文章是去年整理的記錄公司內部wiki上
一直對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的編譯工具鏈還是很強大的,十分有利於程式的最佳化。
如果有任何疑問,歡迎提出,
隨時更新
(這篇文章是去年整理的,記錄公司內部wiki上~)