golang排程模型
執行緒模型
核心級執行緒模型(KSE(Kernel Scheduling Entity))
關鍵點:完全靠作業系統排程
每一個使用者執行緒繫結一個實際的核心執行緒,而執行緒的排程則完全交付給作業系統核心去做,應用程式對執行緒的建立、終止以及同步都基於核心提供的系統呼叫來完成
使用者級執行緒模型
關鍵點:完全靠自己排程
使用者執行緒與核心執行緒KSE是多對一(N : 1)的對映模型,多個使用者執行緒的一般從屬於單個程序
的排程是由使用者自己的執行緒庫來完成,執行緒的建立、銷燬以及多執行緒之間的協調等操作都是由使用者自己的執行緒庫來負責而無須藉助系統呼叫來實現。作業系統只知道使用者程序而對其中的執行緒是無感知的,核心的所有排程都是基於使用者程序。
兩級(混合型)執行緒模型
關鍵點:自身排程與系統排程協同工作
使用者執行緒與核心KSE是多對多(N : M)的對映模型:
首先,區別於使用者級執行緒模型,兩級執行緒模型中的一個程序可以與多個核心執行緒KSE關聯,於是程序內的多個執行緒可以繫結不同的KSE,這點和核心級執行緒模型相似;
其次,又區別於核心級執行緒模型,它的程序裡的所有執行緒並不與KSE一一繫結,而是可以動態繫結不同KSE, 當某個KSE因為其繫結的執行緒的阻塞操作被核心排程出CPU時,其關聯的程序中其餘使用者執行緒可以重新與其他KSE繫結執行
GPM模型
基本概念
OS執行緒抽象,代表著真正執行計算的資源, 每一個goroutine
實際上就是在M
中執行,M
的數量目前最多10000
個.
M
並不儲存G
的狀態, 與G
本身並沒有關係, 所以G
可以在不同的M
執行
分配程式執行的上下文環境, 數量<=
核心數量, 即同時能夠並行執行的G
的數量,相對於G
而言,P
的角色相當於CPU.
程式程式碼中的每一次使用關鍵字go
執行函式其實都生成了一個G
,並將之加入到本地的G
佇列中, 之後M
會生成G
執行的上下文也就是繫結P
來執行函式.
G
維護者goroutine需要的棧、程式計數器以及它所在的M等資訊。
- Seched
代表著一個排程器 它維護有儲存空閒的M
佇列和空閒的P
佇列,可執行的G
佇列,自由的G
佇列以及排程器的一些狀態資訊等。
模型排程
1.P
如何獲得G
排程器Seched
生成一個M
, 然後M
需要持有(繫結)一個P
,接著M
會啟動一個OS執行緒,迴圈讓P
會首先從自己的本地佇列(Local Quequ)中取可執行(Runnable)的G
執行, 如果本地佇列中沒有, 則會從全域性佇列(Globle Queue)中取G
, 如果還沒有, 則會從其他的P
的本地佇列中取一半的佇列放入自己本地佇列之中
2.M
執行函式遇到阻塞,如何處理
實際程式碼執行中可能存在下面的問題,導致程式阻塞
blocking syscall (for example opening a file) network input channel operations primitives in the sync package
主要可歸為兩類
- 使用者態阻塞/喚醒
當goroutine
因為channel操作或者network I/O而阻塞時(實際上golang已經用netpoller實現了goroutine網路I/O阻塞不會導致M被阻塞,僅阻塞G,這裡僅僅是舉個栗子),對應的G會被放置到某個wait
佇列(如channel的waitq),該G的狀態由_Gruning
變為_Gwaitting
,而M會跳過該G嘗試獲取並執行下一個G,如果此時沒有runnable的G供M執行,那麼M將解綁P,並進入sleep
狀態;當阻塞的G被另一端的G2喚醒時(比如channel的可讀/寫通知),G被標記為runnable,嘗試加入G2所在P的runnext,然後再是P的Local佇列和Global佇列。
- 系統呼叫阻塞
當G被阻塞在某個系統呼叫上時,此時G會阻塞在_Gsyscall
狀態,M也處於block on syscall
狀態,此時的M可被搶佔排程:執行該G的M會與P解綁,而P則嘗試與其它idle
的M繫結,繼續執行其它G。如果沒有其它idle
的M,但P的Local佇列中仍然有G需要執行,則建立一個新的M;當系統呼叫完成後,G會重新嘗試獲取一個idle
的P進入它的Local佇列恢復執行,如果沒有idle的P,G會被標記為runnable加入到Global佇列。
排程使用了名叫work stealing
的演算法, 這種演算法適用場景是任務之間的耗時相差比較大,即有的任務很耗時,有的任務很快完成,用這種用演算法很合適;如果任務的耗時很平均則不適合,因為竊取任務也是需要搶佔鎖的,會造成額外的消耗。