淺析Golang的線程模型與調度器


文章目錄

  • Go並發特色
  • Go線程模型
    • GMP模型
    • Go運行時系統的核心元素容器
  • Go調度器
    • 調度器基本數據結構
    • 調度器的一整輪調度
    • 一整輪調度子流程(全力查找可運行的G)
    • 一整輪調度子流程(啟用/停止M) 
  • 系統監測任務

Go並發特色

  Go在內核線程之上,搭建了一個特有的兩級線程模型。除了內核對內核線程的調度之外,Go語言運行時還通過調度器對非內核的goroutine進行調度。

  Go不推薦用共享內存方式來通信,推薦使用通信的方式來共享內存。這里涉及到兩個數據結構,一個是goroutine,另一個channel。channel用於多個goroutine之間傳遞數據,且保證整個過程的並發安全性。

  goroutine 是Go的應用程序級別線程;channel是Go特有一種數據結構,可以理解為一個管道。

Go線程模型

  Go的線程模型由三個核心元素做支撐,它們分別是“G”【goroutine】,“M”【內核線程】,“P”【G的上下文環境】。一個G的執行需要P和M的支持,一個M與一個P關聯后,就形成了G的運行環境(內核線程+上下文環境)。

  M(machine)

    M代表一個操作系統內核線程。它的數據結構如下:

 g0 一個特殊的goroutine,在Go運行時系統啟動之初創建,執行一些運行時任務 
 mstartfn函數  新創建的M上啟動某個特殊任務(系統監控,GC輔助,M自旋)
 curg  M正在運行的G的指針
 p  指向與M關聯的P
 nextp  與M預關聯的P
 spinning  M是否正在尋找可運行的G,這個過程,M自旋
 lockedg  與當前M鎖定的G

 

   

 

 

 

 

 

 

 

    M初始化流程如下:

 

  P(processor)

    P代表執行Go 代碼片段所需要的資源,或者稱之為上下文環境,P與M建立連接后,使P中可運行的G獲得運行時機並執行。

    調用runtime.GOMAXPROCS 函數並傳入期望設置的P的數量 或者 go程序運行前設置環境變量 GOMAXPROCS。P的數量即可運行G隊列的數量。(P默認數量與CPU總核心數相同,最大數量可設置為256)

    P存在如下的狀態:

Pidle 當前P未與任何M關聯
Prunning 當前P正在與某個M關聯
Psyscall 當前P運行的G在進行系統調用
Pgcstop Go運行時系統要求停止調度,P被設置為此狀態
Pdead 當前P不會再被使用(等待GC回收)

 

 

 

 

 

 

    P的內部結構:

可執行G隊列(runtime.p.runq) 待調度的G會在這里
自由G列表(runtime.p.gfree) 已經執行完成的G會存到這里(待運行的任務會優先從這里獲取G,提高G的復用率)

 

    

 

    P的運作流程如下:

  G(goroutine)

    G代表一個Go的代碼片段,使用“go”語句向Go運行時系統提交一個並發任務,Go運行時並發的執行這個任務。

    G存在如下的狀態:

Gidle
當前G剛被新分配,未初始化
Grunnale
當前G在可運行隊列中等待運行
Grunning
當前G正在被運行
Gsycall
當前G在執行某個系統調用
Gwating
當前G正在阻塞
Gdead
當前G正在閑置
Gcopystack
當前G正在被移動(G的棧擴容或者縮容)

 

 

 

 

 

 

 

 

    G的初始化流程如下:

    

    G的運作流程如下:

   

    在M的生命周期內,一個M關聯一個內核線程(下圖KSE),M和P相互引用,一個P可對應多個G。P中可運行G隊列依次傳遞給與P關聯的M,並且獲得運行時機,執行任務。在Go線程模型中,三者之間的關系如下圖所示:

                 

  

  Go運行時系統的核心元素容器

     Go運行時系統通過G,M,P三個核心元素支撐線程模型,在這基礎之上,還有針對這三個核心元素的容器,來對這些元素進行管理,以及供調度器的使用,如下所示:

全局M列表(runtime.allm)
存放運行時系統所有的M
全局P列表(runtime.allp)
存放運行時系統所有P的指針
全局G列表(runtime.allgs)
存放運行時系統所有的G
調度器的空閑M列表(runtime.sched.midle)
存放調度器上所有的空閑的M
調度器的空閑P列表(runtime.sched.pidle)
存放調度器上所有的空閑的P
調度器的可運行G列表(runtime.sched.runqhead | runtime.sched.runqtail)
存放調度器上所有的可運行的G
調度器的自由G列表(runtime.sched.gfreeStack | runtime.sched.gfreeNoStack )
存放調度器上所有的空閑的G。G執行完畢放回自由G列表時,運行時系統檢查G的棧空間大小是否時初始大小,不是的話,就釋放掉,讓G變為無棧的,為了節約資源。同時,從自由G列表獲取G時,也會檢查G是否有棧,沒有的話就初始化棧空間。
P的可運行G列表(runtime.p.runq)
存放正在運行G的M所關聯的P的可運行的G
P的自由G列表(runtime.p.gfree)
存放正在運行G的M所關聯的P的空閑的G

 

 

 

 

 

 

 

 

 

 

 

Go的調度器

  調度器作用於兩級線程模型中,非操作系統內核之外的調度任務。它主要的調度對象為M,P,G的實例,通過核心元素容器作為輔助設施。

  調度器基本數據結構

空閑M列表
 
空閑P列表  
可運行G列表  
自由G列表  
gcwaiting字段(uint32) 是否需要因一些任務而停止調度。在停止調度前,該字段被設置為1,恢復調度前,被設置為0。當字段為1時,調度任務執行時會把當前所有的P狀態設置為Pgcstop
stopwait字段(int32) 需要停止但是仍未停止的P的數量。當gcwaiting為1時,調度任務將一個P狀態設置為Pgcstop,同時將此字段的值減1。該字段值為0,則表示所有的P狀態為Pgcstop
stopnote字段(note) 實現與stopwait相關的事件通知機制。當所有P狀態為Pgcstop,根據該字段喚醒因等待調度停止而暫停的串行運行任務(Go運行時系統中的一些任務運行前需要調度器暫停調度,此類任務稱為串行運行任務
sysmonwait字段(uint32) 停止調度期間,系統監測任務(稍后解釋)是否在等待。0為未暫停,1為暫停
sysmonnote字段(note) 實現與sysmonwait相關的事件通知機制。調度器調度之前,根據sysmonwait字段狀態,決定是否用sysmonnote字段恢復系統監測任務執行

 

 

 

 

 

 

 

 

 

 

 

 

  調度器的一整輪調度

    Go的引導程序會做一系列的初始化工作,初始化工作完成后,會讓調度器進行一整輪的調度(執行封裝了main函數的G)。

    一整輪調度的執行流程如下所示:

    一整輪調度的執行發生在用戶程序啟動時,一系列的初始化工作之后某個G的運行時阻塞、結束、退出系統調用、棧增長用戶程序對某些標准庫的函數調用(runtime.Gosched、runtime.Goexit)等。

    一整輪調度有兩個比較重要的子流程,分別是全力查找可運行的G啟用/停止M,下面對這兩個子流程做做相關介紹。

  全力查找可運行的G

    全力查找可運行的G會多次嘗試從其他地方獲取G(runtime.findrunnable函數,包括從調度器的可運行G列表或者其他P的可運行G列表等),整個過程分為兩個階段,十個步驟。

    第一階段:

    1. 獲取執行終結器的G。Go的運行時有一個特殊的G負責執行終結函數(終結函數一般泛指那些占用了計算機本機資源對象變為垃圾時釋放本機資源的函數),調度器會判斷這個G已完成任務后,獲取到它,放入本地P的可執行隊列(同時把G狀態設置為Grunnable)。
    2. 從本地P可運行G隊列,獲取G。
    3. 從調度器的可運行G隊列,獲取G。
    4. 從netpoller處獲取G。當netpoller已被初始化且已有網絡IO操作,會從netpoller拿到一個G列表,並把列表表頭的G返回,其他的G放入調度器的可運行G隊列。(這里即便拿不到G也會跳過,是非阻塞的
    5. 從其他P可運行G隊列獲取G。從其他P可運行G隊列獲取G時候,會遍歷所有P,一次性拿這個P可執行G隊列的一半G(拿到即返回,拿G時候會用到鎖【原子操作】)。

    第二階段:

    1. 獲取執行GC標記的任務G。假如現在正處於GC標記階段且本地P可以用於GC標記任務(gcMarkWorkAvailable函數),則GC標記專用G返回(G狀態設置為Grunnable)。
    2. 從調度器的可運行G隊列,獲取G。這一次再獲取不到G,就將解除本地P和M的關聯,並把P放入調度器的空閑P列表。
    3. 從全局P列表的每個P的可運行G隊列獲取G。遍歷全局P列表,並檢查P的可運行G隊列,假如可運行G列表不為空,就講持有該G列表的P取出並與M關聯,再返回第一階段的第一個步驟搜索可運行的G。
    4. 獲取執行GC標記的任務G。判斷GC是否處於標記階段,以及GC標記任務相關的全局資源(gcBlackenEnabled字段標示)是否可用,假如條件都達成,調度器會從空閑P列表拿到一個P且與當前M關聯,將GC標記專用G返回(G狀態設置為Grunnable)。
    5. 從netpoller處獲取G。這里與第四部基本相同,假如netpoller已被初始化且已有網絡IO操作,會從netpoller的G列表表頭G返回,但是,這里是阻塞的(也就是說,會等待netpoller的可用G出現)。網絡IO讀取時候,操作系統內核會做一些處理,這時候,IO讀取的G會被轉入Gwaiting狀態。內核處理完成,會返回相應的事件,此時netpoller會通知那些Gwaiting的G。接收到通知的G也就代表可以進行網絡讀寫操作了,繼而被調度器設置為Grunnable狀態,等待執行。

     在經歷過十個步驟后,仍然沒有得到可用G,則當前M會被調度器停止,放入調度器的空閑M列表。

  啟用/停止M

    Go運行時系統起停M的根據一系列的函數完成,如下所示:

stopm()  停止當前M的執行,直到因有新的G變得可運行而被喚醒
gcstopm() 為串行運行任務的執行讓路,停止當前M的執行。串行運行任務執行完畢后喚醒該M
stoplockemd() 停止與某個G鎖定的當前M的執行,直到G可運行時被喚醒
startlockedm(gp *g) 喚醒與gp鎖定的M,讓M去執行這個G(gp)
startm(_p_ *p, spinning bool) 喚醒或創建一個M去關聯P(_p_)並執行

 

 

 

 

 

 

    在了解了Go運行時系統啟停M的相關函數后,看下Go調度器對起停M的執行流程:

系統監測任務

  Go運行時系統使用sysmon函數實現一個系統的監測任務,它在在Go程序生命周期內根據周期時間,周而復始的運行。它主要做如下四件事:

  1. 搶奪符合條件的P和G
    搶奪有兩種方式,第一種為從netpoller處獲取可執行G,與一整輪調度的從netpoller處獲取G方式類似;第二種為搶奪調度器符合條件的P和G。它會遍歷全局P列表,假如P的狀態為Psyscall,且P的可執行G隊列里面存在可執行G(且沒有spinning狀態的M),就把這個P設置為Pidle狀態,等待調度系統將該P與M關聯。假如P狀態為Prunning,且存在G運行時間過長,則系統監測任務告知調度器,當前G運行時間過長,期望停止該G運行其他G(調度器不一定停止,這里監測任務只負責告知)。
  2. 進行強制GC
    Go運行時系統在調度器初始化時會開啟一個專用強制GC的G,它一般處於暫停狀態。一旦GC當前未執行且距離上次執行時間已超過GC最大時間間隔,系統監測任務就會把這個G恢復並放入調度器的可運行G隊列。
  3. 需要時清掃托管堆
  4. 打印調試器跟蹤信息

  最后總結下,Go語言運行時,通過核心元素G,M,P 和 自己的調度器,實現了自己的並發線程模型。調度器通過對G,M,P的調度實現了兩級線程模型中操作系統內核之外的調度任務。整個調度過程中會在多種時機去觸發最核心的步驟 “一整輪調度”,而一整輪調度中最關鍵的部分在“全力查找可運行G”,它保證了M的高效運行(換句話說就是充分使用了計算機的物理資源),一整輪調度中還會涉及到M的啟用停止。最后別忘了,還有一個與Go程序生命周期相同的系統監測任務來進行一些輔助性的工作。

 

  資料參考 :

  https://github.com/golang/go/blob/master/src/runtime/proc.go

  https://book.douban.com/subject/27016236/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM