Go: GoRoutine是如何实现的?
Go runtime的調度器:
在了解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在多個內核線程上跑,這個看似可以集齊上面兩者的優勢,但是無疑增加了調度的難度。
Go的調度器內部有三個重要的結構:M,P,S
M:代表真正的內核OS線程,和POSIX里的thread差不多,真正干活的人
G:代表一個goroutine,它有自己的棧,instruction pointer和其他信息(正在等待的channel等等),用于調度。
P:代表調度的上下文,可以把它看做一個局部的調度器,使go代碼在一個線程上跑,它是實現從N:1到N:M映射的關鍵。
圖中看,有2個物理線程M,每一個M都擁有一個context(P),每一個也都有一個正在運行的goroutine。
P的數量可以通過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。
圖中的M1可能是被創建,或者從線程緩存中取出。
當MO返回時,它必須嘗試取得一個context P來運行goroutine,一般情況下,它會從其他的OS線程那里steal偷一個context過來,
如果沒有偷到的話,它就把goroutine放在一個global runqueue里,然后自己就去睡大覺了(放入線程緩存里)。Contexts們也會周期性的檢查global runqueue,否則global runqueue上的goroutine永遠無法執行。
另一種情況是P所分配的任務G很快就執行完了(分配不均),這就導致了一個上下文P閑著沒事兒干而系統卻任然忙碌。但是如果global runqueue沒有任務G了,那么P就不得不從其他的上下文P那里拿一些G來執行。一般來說,如果上下文P從其他的上下文P那里要偷一個任務的話,一般就‘偷’run queue的一半,這就確保了每個OS線程都能充分的使用。
總結
以上是生活随笔為你收集整理的Go: GoRoutine是如何实现的?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Shell 变量的作用域
- 下一篇: Go: init()执行顺序问题