在瞭解Go的運行時的scheduler之前,需要先瞭解為什麼需要它,因為我們可能會想,OS核心不是已經有一個線程scheduler了嘛?
熟悉POSIX API的人都知道,POSIX的方案在很大程度上是對Unix process進場模型的一個邏輯描述和擴充,兩者有很多相似的地方。 Thread有自己的訊號掩碼,CPU affinity等。但是很多特徵對於Go程式來說都是累贅。 尤其是context環境切換的耗時。另一個原因是Go的記憶體回收需要所有的goroutine停止,使得記憶體在一個一致的狀態。記憶體回收的時間點是不確定的,如果依靠OS自身的scheduler來調度,那麼會有大量的線程需要停止工作。
單獨的開發一個Go的調度器,可以是其知道在什麼時候記憶體狀態是一致的,也就是說,當開始記憶體回收時,運行時只需要為當時正在CPU核上啟動並執行那個線程等待即可,而不是等待所有的線程。
使用者空間線程和核心空間線程之間的映射關係有:N:1、1:1和M:N
N:1是說,多個(N)使用者線程始終在一個核心線程上跑,context環境切換確實很快,但是無法真正的利用多核。
1:1是說,一個使用者線程就只在一個核心線程上跑,這時可以利用多核,但是上下文switch很慢。
M:N是說, 多個goroutine在多個核心線程上跑,這個看似可以集齊上面兩者的優勢,但是無疑增加了調度的難度。
<img src=”https://pic2.zhimg.com/50/2f5c6ef32827fb4fc63c60f4f5314610_hd.jpg” data-rawwidth=”391″ data-rawheight=”103″ class=”content_image” width=”391″>
Go的調度器內部有三個重要的結構:M,P,G
M:代表真正的核心OS線程,和POSIX裡的thread差不多,真正幹活的人
G:代表一個goroutine,它有自己的棧,instruction pointer和其他資訊(正在等待的channel等等),用於調度。
P:代表調度的上下文,可以把它看做一個局部的調度器,使go代碼在一個線程上跑,它是實現從N:1到N:M映射的關鍵。
<img src=”https://pic3.zhimg.com/50/67f09d490f69eec14c1824d939938e14_hd.jpg” data-rawwidth=”400″ data-rawheight=”391″ class=”content_image” width=”400″>
圖中看,有2個物理線程M,每一個M都擁有一個context(P),每一個也都有一個正在啟動並執行goroutine。
P的數量可以通過runtime.GOMAXPROCS()來設定,它其實也就代表了真正的並發度,即有多少個goroutine可以同時運行。
圖中灰色的那些goroutine並沒有運行,而是出於ready的就緒態,正在等待被調度。P維護著這個隊列(稱之為runqueue),
Go語言裡,啟動一個goroutine很容易:go function 就行,所以每有一個go語句被執行,runqueue隊列就在其末尾加入一個
goroutine,在下一個調度點,就從runqueue中取出(如何決定取哪個goroutine?)一個goroutine執行。
為何要維護多個上下文P?因為當一個OS線程被阻塞時,P可以轉而投奔另一個OS線程!
圖中看到,當一個OS線程M0陷入阻塞時,P轉而在OS線程M1上運行。調度器保證有足夠的線程來運行所有的context P。
<img src=”https://pic1.zhimg.com/50/f1125f3027ebb2bd5183cf8c9ce4b3f2_hd.jpg” data-rawwidth=”550″ data-rawheight=”400″ class=”origin_image zh-lightbox-thumb” width=”550″ data-original=”https://pic1.zhimg.com/f1125f3027ebb2bd5183cf8c9ce4b3f2_r.jpg”>
圖中的M1可能是被建立,或者從線程緩衝中取出。當MO返回時,它必須嘗試取得一個context P來運行goroutine,一般情況下,它會從其他的OS線程那裡steal偷一個context過來,
如果沒有偷到的話,它就把goroutine放在一個global runqueue裡,然後自己就去睡大覺了(放入線程緩衝裡)。Contexts們也會周期性的檢查global runqueue,否則global runqueue上的goroutine永遠無法執行。
<img src=”https://pic3.zhimg.com/50/31f04bb69d72b72777568063742741cd_hd.jpg” data-rawwidth=”550″ data-rawheight=”400″ class=”origin_image zh-lightbox-thumb” width=”550″ data-original=”https://pic3.zhimg.com/31f04bb69d72b72777568063742741cd_r.jpg”>
另一種情況是P所分配的任務G很快就執行完了(分配不均),這就導致了一個上下文P閑著沒事兒幹而系統卻任然忙碌。但是如果global runqueue沒有任務G了,那麼P就不得不從其他的上下文P那裡拿一些G來執行。一般來說,如果上下文P從其他的上下文P那裡要偷一個任務的話,一般就‘偷’runqueue的一半,這就確保了每個OS線程都能充分的使用。
參考連結:https://www.zhihu.com/question/20862617http://morsmachine.dk/go-scheduler