GM到GMP,Golang經歷了什麼?

ssdlh 發表於 2021-05-12
Go

超超和麵試官聊完了程式到協程發展史之後,面試官似乎想在GMP模型上對超超“痛下殺手”,下面來看超超能不能接住面試官的大殺器吧!

GM模型

面試官:你知道GMP之前用的是GM模型嗎?

超超:這個我知道,在12年的go1.1版本之前用的都是GM模型,但是由於GM模型效能不好,飽受使用者詬病。之後官方對排程器進行了改進,變成了我們現在用的GMP模型。

面試官:那你能給我說說什麼是GM模型?為什麼效率不好呢?

考點:GM模型

超超:GM模型中的G全稱為Goroutine協程,M全稱為Machine核心級執行緒,排程過程如下

圖片

M(核心執行緒)從加鎖的Goroutine佇列中獲取G(協程)執行,如果G在執行過程中建立了新的G,那麼新的G也會被放入全域性佇列中。

很顯然這樣做有倆個缺點,一是排程,返回G都需要獲取佇列鎖,形成了激烈的競爭。二是M轉移G沒有把資源最大化利用。比如當M1在執行G1時,M1建立了G2,為了繼續執行G1,需要把G2交給M2執行,因為G1和G2是相關的,而暫存器中會儲存G1的資訊,因此G2最好放在M1上執行,而不是其他的M。

GMP

面試官:那你能給我說說GMP模型是怎麼設計的嗎?

考點:GMP設計

超超:G全稱為Goroutine協程,M全稱為Machine核心級執行緒,P全稱為Processor協程執行所需的資源,他在GM的基礎上增加了一個P層,下面我們來看一下他是如何設計的。

圖片

全域性佇列:當P中的本地佇列中有協程G溢位時,會被放到全域性佇列中。

P的本地佇列:P內建的G佇列,存的數量有限,不超過256個。這裡有倆種特殊情況。一是當佇列P1中的G1在執行過程中新建G2時,G2優先存放到P1的本地佇列中,如果佇列滿了,則會把P1佇列中一半的G移動到全域性佇列。二是如果P的本地佇列為空,那麼他會先到全域性佇列中獲取G,如果全域性佇列中也沒有G,則會嘗試從其他執行緒繫結的P中偷取一半的G。

面試官:P和M數量是可以無限擴增的嗎?

考點:GMP細節

超超:是不能無限擴增的,無限擴增系統也承受不了呀,哈哈

P的數量:由啟動時環境變數$GOMAXPROCS或者是由runtime的方法GOMAXPROCS()決定。

M的數量:go程式啟動時,會設定M的最大數量,預設10000。但是核心很難建立出如此多的執行緒,因此預設情況下M的最大數量取決於核心。也可以呼叫runtime/debug中的SetMaxThreads函式,手動設定M的最大數量。

面試官:那P和M都是在程式執行時就被建立好了嗎?

考點:繼續深挖GMP細節

超超:P和M建立的時機是不同的

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

M何時建立:核心級執行緒的初始化是由核心管理的,當沒有足夠的M來關聯P並執行其中的可執行的G時會請求建立新的M。比如M在執行G1時被阻塞住了,此時需要新的M去繫結P,如果沒有在休眠的M則需要新建M。

圖片

面試官:你能給我說說當M0將G1執行結束後會怎樣做嗎?

考點:G在GMP模型中流動過程

超超:那我給你舉個例子吧(:這次把整個過程都說完,看你還能問什麼

圖片

(圖轉自劉丹冰Golang的協程排程器原理及GMP設計思想)

1. 呼叫 go func()建立一個goroutine;

2. 新建立的G優先儲存在P的本地佇列中,如果P的本地佇列已經滿了就會儲存在全域性的佇列中;

3. M需要在P的本地佇列彈出一個可執行的G,如果P的本地佇列為空,則先會去全域性佇列中獲取G,如果全域性佇列也為空則去其他P中偷取G放到自己的P中

4. G將相關引數傳輸給M,為M執行G做準備

5. 當M執行某一個G時候如果發生了系統呼叫產生導致M會阻塞,如果當前P佇列中有一些G,runtime會將執行緒M和P分離,然後再獲取空閒的執行緒或建立一個新的核心級的執行緒來服務於這個P,阻塞呼叫完成後G被銷燬將值返回;

6. 銷燬G,將執行結果返回

7. 當M系統呼叫結束時候,這個M會嘗試獲取一個空閒的P執行,如果獲取不到P,那麼這個執行緒M變成休眠狀態, 加入到空閒執行緒中。

GM與GMP

面試官:看來你對GMP整個流程還是比較清楚的,那你再給我說說GMP相對於GM做了哪些優化吧。

考點:GM與GMP區別

超超:優化點有三個,一是每個 P 有自己的本地佇列,而不是所有的G操作都要經過全域性的G佇列,這樣鎖的競爭會少的多的多。而 GM 模型的效能開銷大頭就是鎖競爭。

圖片

二是P的本地佇列平衡上,在 GMP 模型中也實現了 Work Stealing 演算法,如果 P 的本地佇列為空,則會從全域性佇列或其他 P 的本地佇列中竊取可執行的 G 來執行(通常是偷一半),減少空轉,提高了資源利用率。

圖片

三是hand off機制當M0執行緒因為G1進行系統呼叫阻塞時,執行緒釋放繫結的P,把P轉移給其他空閒的執行緒M1執行,同樣也是提高了資源利用率。

圖片

面試官:你有沒有想過佇列和執行緒的優化可以做在G層和M層,為什麼要加一個P層呢?

考點:深挖GMP

超超:這是因為M層是放在核心的,我們無權修改,在前面協程的問題中回答過,核心級也是使用者級執行緒發展成熟才加入核心中。所以在M無法修改的情況下,所有的修改只能放在使用者層。將佇列和M繫結,由於hand off機制M會一直擴增,因此佇列也需要一直擴增,那麼為了使Work Stealing 能夠正常進行,佇列管理將會變的複雜。因此設定了P層作為中間層,進行佇列管理,控制GMP數量(最大個數為P的數量)。

圖片

面試官:你對GMP還是蠻瞭解的哈,那回到剛開始的話題,你知道mac中的回收站只能單開,訪達視窗可以多開吧?

超超:知道呀,這是單例模式(:為什麼mac這個點過不去了😫

未完待續~

圖片

如果你有什麼問題想問超超,歡迎新增我的微信,進讀者群和超超一起討論呀!

圖片

本作品採用《CC 協議》,轉載必須註明作者和本文連結