Go runtime的调度器
為什么80%的碼農都做不了架構師?>>> ??
著作權歸作者所有。
商業(yè)轉載請聯(lián)系作者獲得授權,非商業(yè)轉載請注明出處。
作者:Yi Wang
鏈接:http://www.zhihu.com/question/20862617/answer/27964865
來源:知乎
Go runtime的調度器:
在了解Go的運行時的scheduler之前,需要先了解為什么需要它,因為我們可能會想,OS內核不是已經(jīng)有一個線程scheduler了嘛?
熟悉POSIX API的人都知道,POSIX的方案在很大程度上是對Unix process進場模型的一個邏輯描述和擴展,兩者有很多相似的地方。 Thread有自己的信號掩碼,CPU affinity等。但是很多特征對于Go程序來說都是累贅。 尤其是context上下文切換的耗時。另一個原因是Go的垃圾回收需要所有的goroutine停止,使得內存在一個一致的狀態(tài)。垃圾回收的時間點是不確定的,如果依靠OS自身的scheduler來調度,那么會有大量的線程需要停止工作。
單獨的開發(fā)一個GO得調度器,可以是其知道在什么時候內存狀態(tài)是一致的,也就是說,當開始垃圾回收時,運行時只需要為當時正在CPU核上運行的那個線程等待即可,而不是等待所有的線程。
用戶空間線程和內核空間線程之間的映射關系有:N:1,1:1和M:N
N:1是說,多個(N)用戶線程始終在一個內核線程上跑,context上下文切換確實很快,但是無法真正的利用多核。
1:1是說,一個用戶線程就只在一個內核線程上跑,這時可以利用多核,但是上下文switch很慢。
M:N是說, 多個goroutine在多個內核線程上跑,這個看似可以集齊上面兩者的優(yōu)勢,但是無疑增加了調度的難度。
<img src="https://pic1.zhimg.com/2f5c6ef32827fb4fc63c60f4f5314610_b.jpg" data-rawwidth="391" data-rawheight="103" class="content_image" width="391">
Go的調度器內部有三個重要的結構:M,P,S
M:代表真正的內核OS線程,和POSIX里的thread差不多,真正干活的人
G:代表一個goroutine,它有自己的棧,instruction pointer和其他信息(正在等待的channel等等),用于調度。
P:代表調度的上下文,可以把它看做一個局部的調度器,使go代碼在一個線程上跑,它是實現(xiàn)從N:1到N:M映射的關鍵。
<img src="https://pic1.zhimg.com/67f09d490f69eec14c1824d939938e14_b.jpg" data-rawwidth="400" data-rawheight="391" class="content_image" width="400">
圖中看,有2個物理線程M,每一個M都擁有一個context(P),每一個也都有一個正在運行的goroutine。
P的數(shù)量可以通過GOMAXPROCS()來設置,它其實也就代表了真正的并發(fā)度,即有多少個goroutine可以同時運行。
圖中灰色的那些goroutine并沒有運行,而是出于ready的就緒態(tài),正在等待被調度。P維護著這個隊列(稱之為runqueue),
Go語言里,啟動一個goroutine很容易:go function 就行,所以每有一個go語句被執(zhí)行,runqueue隊列就在其末尾加入一個
goroutine,在下一個調度點,就從runqueue中取出(如何決定取哪個goroutine?)一個goroutine執(zhí)行。
為何要維護多個上下文P?因為當一個OS線程被阻塞時,P可以轉而投奔另一個OS線程!
圖中看到,當一個OS線程M0陷入阻塞時,P轉而在OS線程M1上運行。調度器保證有足夠的線程來運行所以的context P。
<img src="https://pic3.zhimg.com/f1125f3027ebb2bd5183cf8c9ce4b3f2_b.jpg" data-rawwidth="550" data-rawheight="400" class="origin_image zh-lightbox-thumb" width="550" data-original="https://pic3.zhimg.com/f1125f3027ebb2bd5183cf8c9ce4b3f2_r.jpg">
圖中的M1可能是被創(chuàng)建,或者從線程緩存中取出。
當MO返回時,它必須嘗試取得一個context P來運行goroutine,一般情況下,它會從其他的OS線程那里steal偷一個context過來,
如果沒有偷到的話,它就把goroutine放在一個global runqueue里,然后自己就去睡大覺了(放入線程緩存里)。Contexts們也會周期性的檢查global runqueue,否則global runqueue上的goroutine永遠無法執(zhí)行。
<img src="https://pic2.zhimg.com/31f04bb69d72b72777568063742741cd_b.jpg" data-rawwidth="550" data-rawheight="400" class="origin_image zh-lightbox-thumb" width="550" data-original="https://pic2.zhimg.com/31f04bb69d72b72777568063742741cd_r.jpg">
另一種情況是P所分配的任務G很快就執(zhí)行完了(分配不均),這就導致了一個上下文P閑著沒事兒干而系統(tǒng)卻任然忙碌。但是如果global runqueue沒有任務G了,那么P就不得不從其他的上下文P那里拿一些G來執(zhí)行。一般來說,如果上下文P從其他的上下文P那里要偷一個任務的話,一般就‘偷’run queue的一半,這就確保了每個OS線程都能充分的使用。
轉載于:https://my.oschina.net/fdhay/blog/617349
總結
以上是生活随笔為你收集整理的Go runtime的调度器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Angular+Flask搭建一个记录工
- 下一篇: Asp.net中的Cache--Http