首頁 >後端開發 >Golang >關於 Golang 協程調度

關於 Golang 協程調度

藏色散人
藏色散人轉載
2020-09-01 14:50:022727瀏覽

以下由golang教學專欄跟大家介紹Golang 協程調度  ,希望對需要的朋友有幫助!

關於 Golang 協程調度

一、執行緒模型

  • N:1模型,N個使用者空間執行緒在1個核心空間執行緒上運行。優勢是上下文切換非常快但是無法利用多核心系統的優點。
  • 1:1模型,1個核心空間執行緒運行一個使用者空間執行緒。這種充分利用了多核心系統的優勢但是上下文切換非常慢,因為每個調度都會在用戶態和核心態之間切換。 (POSIX線程模型(pthread),Java)
  • M:N模型, 每個用戶線程對應多個核心空間線程,同時也可以一個核心空間線程對應多個用戶空間線程。 Go打算採用這種模型,使用任意個核心模型管理任意goroutine。這樣結合了以上兩種模型的優點,但缺點就是調度的複雜度。

下面看看golang的協程調度

  • M:一個使用者空間線程,同時對應一個核心線程,類似posix pthread
  • P:代表運行的上下文環境, 也就是我們上一節實現的調度器,一個調度器也會對應一個就緒隊列
  • G:goroutine,即協程

二、調度模型簡介

groutine能擁有強大的並發實作是透過GPM調度模型實現,以下就來解釋下goroutine的調度模型。

Go的調度器內部有三個重要的結構:M,P,G
M:M是核心級執行緒的封裝,數量對應真實的CPU數,一個M就是一個線程,goroutine就是跑在M之上的;M是一個很大的結構,裡面維護小物件記憶體cache(mcache)、目前執行的goroutine、隨機數產生器等等非常多的資訊
G:代表一個goroutine,它有自己的棧,instruction pointer和其他資訊(正在等待的channel等等),用於調度。
P:P全名為Processor,處理器,它的主要用途就是用來執行goroutine的。每個Processor物件都擁有一個LRQ(Local Run Queue),未分配的Goroutine物件保存在GRQ(Global Run Queue )中,等待分配給某一個P的LRQ中,每個LRQ裡包含若干個使用者建立的Goroutine對象。

Golang採用的是多線程模型,更詳細的說他是一個兩級線程模型,但它對系統線程(內核級線程)進行了封裝,暴露了一個輕量級的協程goroutine (使用者級執行緒)供使用者使用,而使用者級執行緒到核心級執行緒的調度由golang的runtime負責,調度邏輯對外透明。 goroutine的優點在於上下文切換在完全用戶態進行,無需像執行緒一樣頻繁在用戶態與內核態之間切換,節省了資源消耗。

調度實作

從上圖看,有2個實體執行緒M,每一個M都有一個處理器P,每一個也都有一個正在運行的goroutine。
P的數量可以透過GOMAXPROCS()來設置,它其實也就代表了真正的並發度,也就是有多少個goroutine可以同時運作。
圖中灰色的那些goroutine並沒有運行,而是出於ready的就緒態,正在等待被調度。 P維護著這個隊列(稱為runqueue),
Go語言裡,啟動一個goroutine很容易:go function 就行,所以每有一個go語句被執行,runqueue隊列就在其末尾加入一個
goroutine ,在下一個調度點,就從runqueue中取出(如何決定取哪個goroutine?)一個goroutine執行。

 

當一個OS執行緒M0陷入阻塞時(如下圖),P轉而在執行M1,圖中的M1可能是正被創建,或是從執行緒快取中取出。

 

當MO回來時,它必須嘗試取得一個P來運行goroutine,一般情況下,它會從其他的OS線程那裡拿一個P過來,
如果沒有拿到的話,它就把goroutine放在一個global runqueue裡,然後自己睡(放入線程快取裡)。所有的P也會週期性的檢查global runqueue並運行其中的goroutine,否則global runqueue上的goroutine永遠無法執行。

 

另一種情況是P所分配的任務G很快就執行完了(分配不均),這就導致了這個處理器P很忙,但是其他的P還有任務,此時如果global runqueue沒有任務G了,那麼P就得從其他的P拿一些G來執行。一般來說,如果P從其他的P那裡要拿任務的話,一般就拿run queue的一半,這確保了每個OS線程都能充分的使用,如下圖:

三、GPM建立相關問題

M和P的數量如何決定?或說何時會創建M和P?

1、P的數量:

  • 由啟動時環境變數$GOMAXPROCS或是由runtime的方法GOMAXPROCS()決定(預設為1)。這意味著在程式執行的任意時刻都只有$GOMAXPROCS個goroutine在同時運作。

2、M的數量:

  • go語言本身的限制:go程式啟動時,會設定M的最大數量,預設10000.但是核心很難支持這麼多的執行緒數,所以這個限制可以忽略。
  • runtime/debug中的SetMaxThreads函數,設定M的最大數量
  • 一個M阻塞了,會建立新的M。

M與P的數量沒有絕對關係,一個M阻塞,P就會去創建或切換另一個M,所以,即使P的預設數量是1,也有可能會創建很多M出來。

3、P何時創建:在確定了P的最大數量n後,運行時系統會根據這個數量建立n個P。

4、M何時創建:沒有足夠的M來關聯P並運行其中的可運行的G。例如所有的M此時都阻塞住了,而P中還有很多就緒任務,就會去尋找空閒的M,而沒有空閒的,就會去創建新的M。

M選擇哪一個P關聯?

  • M會選擇導致此M被建立的那個P關聯。

什麼時候會切換P與M的關聯關係?

當M因係統呼叫而阻塞時(M上運行的G進入了系統呼叫的時候),M與P會分開,如果此時P的就緒佇列中還有任務,
P就會去關聯一個空閒的M,或是建立一個M進行關聯。 (也就是說go不是像libtask一樣處理IO阻塞的?不確定。)

就緒的G如何選擇進入哪個P的就緒佇列?

  • 預設:因為P的預設數量是1(M不一定是1),所以如果我們不改變GOMAXPROCS,無論我們在程式中用go語句建立多少個goroutine,它們都只會被塞入同一個P的就緒佇列中。
  • 有多個P的情況下:如果修改了GOMAXPROCS或呼叫了runtime.GOMAXPROCS,執行時間系統會把所有的G均勻的分佈在各個P的就緒佇列中。

如何確保每個P的就緒佇列中都會有G

如果一個P的就緒佇列所有任務都執行完了,那麼P會嘗試從其他P的就緒佇列中取出一部分到自己的就緒佇列中,以確保每個P的就緒佇列都有任務可以執行。

以上是關於 Golang 協程調度的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:cnblogs.com。如有侵權,請聯絡admin@php.cn刪除