Golang調(diào)度器GMP原理小結(jié)
?一、Golang “調(diào)度器” 的由來?
1.1 單進(jìn)程時(shí)代不需要調(diào)度器
每個(gè)程序就是一個(gè)進(jìn)程,知道一個(gè)程序運(yùn)行結(jié)束,下一個(gè)進(jìn)程才會(huì)執(zhí)行,一切的程序只能串行發(fā)生

弊端:
- 單一的執(zhí)行流程,計(jì)算機(jī)只能一個(gè)任務(wù)一個(gè)任務(wù)處理
- 進(jìn)程阻塞(IO訪問等)所帶來的CPU時(shí)間浪費(fèi)
1.2 多進(jìn)程/線程時(shí)代有了調(diào)度器需求(TODO 進(jìn)程和線程的區(qū)別)

一個(gè)進(jìn)程阻塞CPU可以立刻切換到其他進(jìn)程中去執(zhí)行(解決阻塞的問題),而且調(diào)度CPU的算法可以保證在運(yùn)行的進(jìn)程都可以被分配到CPU的運(yùn)行時(shí)間片。(宏觀看,似乎多個(gè)進(jìn)程是在同時(shí)被運(yùn)行)
弊端:
- 進(jìn)程的創(chuàng)建、切換、銷毀,都會(huì)占用很長的時(shí)間,進(jìn)程過多會(huì)導(dǎo)致CPU利用率低,都拿去做進(jìn)程調(diào)度了
- 高內(nèi)存占用(進(jìn)程虛擬內(nèi)存會(huì)占用4GB【32位操作系統(tǒng)】,而線程雖然占用小一點(diǎn),但是也需要大約4MB)。

1.3 協(xié)程來提高CPU利用率
優(yōu)點(diǎn):協(xié)程在用戶態(tài)線程完成切換調(diào)度,不會(huì)陷入到內(nèi)核態(tài),切換輕量快速
一個(gè)“用戶態(tài)線程”必須綁定一個(gè)“內(nèi)核態(tài)線程”,但是CPU并不知道有“用戶態(tài)線程”的存在,它只知道它運(yùn)行的是一個(gè)“內(nèi)核態(tài)線程”(Linux的PCB的進(jìn)程控制塊)。

再細(xì)分,內(nèi)核線程依然叫“線程(Thread)”,用戶態(tài)線程叫協(xié)程(co-routine)

既然一個(gè)協(xié)程(co-routine)可以綁定一個(gè)線程(Thread),那么能不能多個(gè)協(xié)程(co-routine)綁定一個(gè)或者多個(gè)線程(Thread)呢?基于以上思考,產(chǎn)生了以下三種協(xié)程和線程的映射關(guān)系;
1.3.1 N:1關(guān)系
N個(gè)協(xié)程綁定1個(gè)線程,即程序啟動(dòng)后只會(huì)創(chuàng)建一個(gè)調(diào)度線程,用來管理N個(gè)協(xié)程;
缺點(diǎn):用不了硬件的多核加速能力,協(xié)程阻塞會(huì)導(dǎo)致線程阻塞,失去并發(fā)能力;

1.3.2 1:1關(guān)系
一個(gè)協(xié)程綁定一個(gè)線程
缺點(diǎn):線程的創(chuàng)建、刪除和切換的代價(jià)都是由CPU來完成的,代價(jià)高,和直接使用多線程方式來說,沒帶來多大提升,有種脫褲子放屁的感覺;

1.3.3 M:N關(guān)系
M個(gè)協(xié)程綁定N個(gè)內(nèi)核線程,是N:1和1:1類型的結(jié)合,保留以上2種模型的優(yōu)點(diǎn)情況下,同時(shí)客服了以上2種模型的缺點(diǎn),實(shí)現(xiàn)較為復(fù)雜;

線程由CPU調(diào)度是搶占式的,協(xié)程由用戶態(tài)調(diào)度是協(xié)作式的,一個(gè)協(xié)程讓出CPU后,才能執(zhí)行下一個(gè)協(xié)程;
1.4 Go語言的協(xié)程goroutine
Go使用的goroutine是來自協(xié)程的概念,讓一組可復(fù)用的函數(shù)運(yùn)行在一組線程之上,及時(shí)有協(xié)程阻塞,該線程的其他協(xié)程也可以被runtime調(diào)度,轉(zhuǎn)移到其他可運(yùn)行的線程上;
優(yōu)點(diǎn):
- 輕量,一個(gè)goroutine只占用幾KB內(nèi)存,能在有限的內(nèi)存空間內(nèi)支持大量goroutine,支持了更多的并發(fā)
- 調(diào)度更靈活(runtime調(diào)度)
1.5 被廢棄的goroutine調(diào)度器
go 2012之前的調(diào)度器被廢棄,可以先看看之前的調(diào)度器存在的問題


M想要執(zhí)行、放回G都必須訪問全局的G隊(duì)列,并且M有多個(gè),即多線程訪問同一資源需要加鎖保證互斥/同步
弊端:
- 創(chuàng)建、銷毀、調(diào)度G都需要每個(gè)M獲取全局隊(duì)列的鎖,形成了激烈的鎖競爭
- 系統(tǒng)調(diào)用(CPU在M之間的切換)導(dǎo)致頻繁的線程阻塞和取消阻塞操作增加了系統(tǒng)開銷
二、Goroutine調(diào)度器的CMP模型的設(shè)計(jì)思想
新調(diào)度器中,除了M(thread)和G(goroutine),又引進(jìn)了P(Processor)

Processor:包含了運(yùn)行g(shù)oroutine的資源,如果線程想運(yùn)行g(shù)oroutine,必須先獲取p,p中還包含了可運(yùn)行的G隊(duì)列
2.1 GMP模型
Go中,線程是運(yùn)行g(shù)oroutine的實(shí)體,調(diào)度器的功能是將可運(yùn)行的goroutine分配到工作線程上;

- 全局隊(duì)列(global queue):存放等待運(yùn)行的G
- P的本地隊(duì)列:同全局隊(duì)列類型,存放的也是等待運(yùn)行的G,存放的數(shù)量有限,不超過256個(gè)。創(chuàng)建G’時(shí),G’優(yōu)先加入到P的本地隊(duì)列,如果本地隊(duì)列滿了,則會(huì)把本地隊(duì)列中一半的G移動(dòng)到全局隊(duì)列中
- P列表:所有的P都在程序啟動(dòng)時(shí)創(chuàng)建,并保存在數(shù)組中,最多有GOMAXPROCS(可配置)個(gè)
- M:線程想運(yùn)行任務(wù)就得獲取P,從P的本地隊(duì)列獲取G,P隊(duì)列為空時(shí),M也會(huì)嘗試從全局隊(duì)列拿一批G放到P的本地隊(duì)列中,或者從其他P的本地隊(duì)列偷一半放到自己的本地隊(duì)列中。M運(yùn)行G,G執(zhí)行之后,M會(huì)從P獲取下一個(gè)G,不斷重復(fù)
Goroutine調(diào)度器和OS調(diào)度器是通過M結(jié)合起來的,每個(gè)M都代表了一個(gè)內(nèi)核線程,OS調(diào)度器負(fù)責(zé)把內(nèi)核線程分配到CPU的核上執(zhí)行
2.1.1 有關(guān)P和M的個(gè)數(shù)問題
2.1.1.1 P
由啟動(dòng)時(shí)環(huán)境變量$GOAMXPROCES或者是由runtime的方法GOMAXPROCES()決定。這意味著在程序執(zhí)行的任意時(shí)刻,都只有$GOMAXPROCES個(gè)goroutine在同時(shí)運(yùn)行;
2.1.1.2 M
- go語言本身的限制:go程序啟動(dòng)時(shí),會(huì)設(shè)置M的最大數(shù)量,默認(rèn)10000,但是內(nèi)核很難支持這么多的線程數(shù),因此這個(gè)限制可以忽略。
- runtime/debug中的SetMaxThreads()函數(shù),可以設(shè)置M的最大數(shù)量。
- 一個(gè)M阻塞了,會(huì)創(chuàng)建新的M。
M與P的數(shù)量沒有絕對(duì)的關(guān)系,一個(gè)M阻塞了,P就會(huì)去創(chuàng)建或者切換另一個(gè)M,所以,即使P的默認(rèn)數(shù)量是1,也有可能會(huì)創(chuàng)建很多個(gè)M出來;
2.1.2 P和M何時(shí)會(huì)被創(chuàng)建
2.1.1.1 P
在確定了P的最大數(shù)量n后,運(yùn)行是系統(tǒng)會(huì)根據(jù)這個(gè)數(shù)量創(chuàng)建n個(gè)p。
2.1.1.2 M
沒有足夠的M來關(guān)聯(lián)P并運(yùn)行其中的可運(yùn)行的G。比如所有的M此時(shí)都被阻塞住了,而P中還有很多就緒任務(wù),就會(huì)去尋找空閑的M,沒有空閑的,就會(huì)去創(chuàng)建新的M。
2.2 調(diào)度器的設(shè)計(jì)策略
復(fù)用線程:避免頻繁的創(chuàng)建、銷毀線程,增加系統(tǒng)開銷,而是對(duì)線程的復(fù)用。
- work stealing機(jī)制:當(dāng)本線程無可以運(yùn)行的G時(shí),嘗試從其他線程綁定的P中偷取G,而不是銷毀線程;
- hand off機(jī)制:當(dāng)本線程因?yàn)镚進(jìn)行系統(tǒng)調(diào)用阻塞時(shí),線程釋放綁定的P,把P轉(zhuǎn)移給其他空閑的線程執(zhí)行。
- 可搶占:goroutine可以被搶占式調(diào)度,防止其他goroutine被餓死
- 全局G隊(duì)列:當(dāng)M執(zhí)行work stealing從其他P中偷不到G時(shí),M可以從全局G隊(duì)列中獲取G
2.3 go func()調(diào)度流程

- 我們通過go func()來創(chuàng)建一個(gè)goroutine;
- 有兩個(gè)存儲(chǔ)G的隊(duì)列,一個(gè)是局部調(diào)度器P的本地隊(duì)列、一個(gè)是全局G隊(duì)列。新創(chuàng)建的G會(huì)先保存在P的本地隊(duì)列中,如果P的本地隊(duì)列已經(jīng)滿了,就會(huì)保存在全局的隊(duì)列中;
- G只能運(yùn)行在M中,一個(gè)M必須持有一個(gè)P,M與P是1:1的關(guān)系。M會(huì)從P的本地隊(duì)列彈出一個(gè)可執(zhí)行的G來執(zhí)行,如果P的本地隊(duì)列為空,就會(huì)嘗試從其他的MP組合中偷取一個(gè)可執(zhí)行的G來執(zhí)行;
- 一個(gè)M調(diào)度G執(zhí)行的過程是一個(gè)循環(huán)機(jī)制;
- 當(dāng)M執(zhí)行一個(gè)G的時(shí)候,如果發(fā)生了syscall或者其余阻塞操作,M會(huì)被阻塞,如果當(dāng)前有一些G在執(zhí)行,runtime會(huì)把這個(gè)線程M從P中摘除,然后再創(chuàng)建一個(gè)新的操作系統(tǒng)級(jí)別的線程(如果有空閑的線程就可以復(fù)用空閑線程)來服務(wù)與這個(gè)P;
- 當(dāng)M系統(tǒng)調(diào)用結(jié)束時(shí)候,這個(gè)G會(huì)嘗試獲取一個(gè)空閑的P執(zhí)行,并放入到這個(gè)P的本地隊(duì)列中。如果獲取不到P,那么這個(gè)線程M會(huì)變成休眠狀態(tài),加入到空閑線程中,然后這個(gè)G會(huì)被放入到全局隊(duì)列中;
2.4 調(diào)度器的生命周期
M0:啟動(dòng)程序后的編號(hào)為 0 的主線程,這個(gè) M 對(duì)應(yīng)的實(shí)例會(huì)在全局變量 runtime.m0 中,不需要在 heap 上分配,M0 負(fù)責(zé)執(zhí)行初始化操作和啟動(dòng)第一個(gè) G, 在之后 M0 就和其他的 M 一樣了。
G0:每次啟動(dòng)一個(gè) M 都會(huì)第一個(gè)創(chuàng)建的 goroutine,G0 僅用于負(fù)責(zé)調(diào)度的 G,G0 不指向任何可執(zhí)行的函數(shù),每個(gè) M 都會(huì)有一個(gè)自己的 G0。在調(diào)度或系統(tǒng)調(diào)用時(shí)會(huì)使用 G0 的??臻g,全局變量的 G0 是 M0 的 G0。
追蹤代碼:
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}- runtime創(chuàng)建最初的線程m0和goroutine g0,并把2者關(guān)聯(lián);
- 調(diào)度器初始化:初始化m0、棧、垃圾回收,以及創(chuàng)建和初始化由GOMAXPROCES個(gè)P構(gòu)成的P列表;
- 實(shí)例代碼中的main函數(shù)是main.main,runtime中也有一個(gè)main函數(shù)——runtime.main,代碼經(jīng)過編譯后,runtime.main會(huì)調(diào)用main.main,程序啟動(dòng)時(shí)會(huì)為runtime.main創(chuàng)建goroutine,稱它為main goroutine吧,然后把main goroutine加入到P的本地隊(duì)列;
- 啟動(dòng)m0,m0已經(jīng)綁定了P,會(huì)從P的本地隊(duì)列中獲取G,獲取到main goroutine;
- G擁有棧,M根據(jù)G中的棧信息和調(diào)度信息設(shè)置運(yùn)行環(huán)境;
- M運(yùn)行G;
- G退出,再次回到M獲取可運(yùn)行的G,這樣重復(fù)下去,直到main.main退出,runtime.main執(zhí)行defer和panic處理,或者調(diào)用runtime.exit退出程序;?
三、Go調(diào)度器調(diào)度場(chǎng)景過程全解析
3.1 場(chǎng)景一
P擁有G1,M1獲取P后開始運(yùn)行G1,G1使用go func()創(chuàng)建了G2,為了局部性,G2優(yōu)先加入到P1的本地隊(duì)列;

3.2 場(chǎng)景二
G1運(yùn)行完成后(函數(shù):goexit),M1上運(yùn)行的goroutine切換為G0,G0負(fù)責(zé)調(diào)度時(shí)協(xié)程的切換(函數(shù):scheduler)。從P的本地隊(duì)列獲取G2,從G0切換到G2,并開始運(yùn)行G2(函數(shù):excute)。實(shí)現(xiàn)了線程M1的復(fù)用;

3.3 場(chǎng)景三
假設(shè)每個(gè)P的本地隊(duì)列只能存3個(gè)G。G2要?jiǎng)?chuàng)建6個(gè)G,前三個(gè)G(G3,G4,G5)已經(jīng)加入到P1的本地隊(duì)列中,此時(shí)P1的本地隊(duì)列滿了;

3.4 場(chǎng)景四
G2 在創(chuàng)建 G7 的時(shí)候,發(fā)現(xiàn) P1 的本地隊(duì)列已滿,需要執(zhí)行負(fù)載均衡 (把 P1 中本地隊(duì)列中前一半的 G,還有新創(chuàng)建 G 轉(zhuǎn)移到全局隊(duì)列)
(實(shí)現(xiàn)中并不一定是新的 G,如果 G 是 G2 之后就執(zhí)行的,會(huì)被保存在本地隊(duì)列,利用某個(gè)老的 G 替換新 G 加入全局隊(duì)列)

這些G被轉(zhuǎn)移到全局隊(duì)列時(shí),會(huì)被打亂順序,所以G3,G4,G7被轉(zhuǎn)移到全局隊(duì)列中的順序無規(guī)則;
3.5 場(chǎng)景五
G2創(chuàng)建G8時(shí),P1的本地隊(duì)列未滿,所以G8會(huì)被加入到P1的本地隊(duì)列中

G8 加入到 P1 點(diǎn)本地隊(duì)列的原因還是因?yàn)?P1 此時(shí)在與 M1 綁定,而 G2 此時(shí)是 M1 在執(zhí)行。所以 G2 創(chuàng)建的新的 G 會(huì)優(yōu)先放置到自己的 M 綁定的 P 上
3.6 場(chǎng)景六
在創(chuàng)建G時(shí),運(yùn)行的G會(huì)嘗試喚醒其他空閑的P和M組合去執(zhí)行;

假定 G2 喚醒了 M2,M2 綁定了 P2,并運(yùn)行 G0,但 P2 本地隊(duì)列沒有 G,M2 此時(shí)為自旋線程(沒有 G 但為運(yùn)行狀態(tài)的線程,不斷尋找 G)
3.7 場(chǎng)景七
M2 嘗試從全局隊(duì)列 (簡稱 “GQ”) 取一批 G 放到 P2 的本地隊(duì)列(函數(shù):findrunnable())。M2 從全局隊(duì)列取的 G 數(shù)量符合下面的公式:
n = min(len(GQ)/GOMAXPROCS + 1, len(GQ/2))
至少從全局隊(duì)列取 1 個(gè) g,但每次不要從全局隊(duì)列移動(dòng)太多的 g 到 p 本地隊(duì)列,給其他 p 留點(diǎn)。這是從全局隊(duì)列到 P 本地隊(duì)列的負(fù)載均衡

假定我們場(chǎng)景中一共有 4 個(gè) P(GOMAXPROCS 設(shè)置為 4,那么我們?cè)试S最多就能用 4 個(gè) P 來供 M 使用)。所以 M2 只從能從全局隊(duì)列取 1 個(gè) G(即 G3)移動(dòng) P2 本地隊(duì)列,然后完成從 G0 到 G3 的切換,運(yùn)行 G3
3.8 場(chǎng)景八
假設(shè) G2 一直在 M1 上運(yùn)行,經(jīng)過 2 輪后,M2 已經(jīng)把 G7、G4 從全局隊(duì)列獲取到了 P2 的本地隊(duì)列并完成運(yùn)行,全局隊(duì)列和 P2 的本地隊(duì)列都空了,如場(chǎng)景 8 圖的左半部分

全局隊(duì)列已經(jīng)沒有 G,那 m 就要執(zhí)行 work stealing (偷取):從其他有 G 的 P 哪里偷取一半 G 過來,放到自己的 P 本地隊(duì)列。P2 從 P1 的本地隊(duì)列尾部取一半的 G,本例中一半則只有 1 個(gè) G8,放到 P2 的本地隊(duì)列并執(zhí)行;
3.9 場(chǎng)景九
G1 本地隊(duì)列 G5、G6 已經(jīng)被其他 M 偷走并運(yùn)行完成,當(dāng)前 M1 和 M2 分別在運(yùn)行 G2 和 G8,M3 和 M4 沒有 goroutine 可以運(yùn)行,M3 和 M4 處于自旋狀態(tài),它們不斷尋找 goroutine。

為什么要讓 m3 和 m4 自旋,自旋本質(zhì)是在運(yùn)行,線程在運(yùn)行卻沒有執(zhí)行 G,就變成了浪費(fèi) CPU. 為什么不銷毀現(xiàn)場(chǎng),來節(jié)約 CPU 資源。因?yàn)閯?chuàng)建和銷毀 CPU 也會(huì)浪費(fèi)時(shí)間,我們希望當(dāng)有新 goroutine 創(chuàng)建時(shí),立刻能有 M 運(yùn)行它,如果銷毀再新建就增加了時(shí)延,降低了效率。當(dāng)然也考慮了過多的自旋線程是浪費(fèi) CPU,所以系統(tǒng)中最多有 GOMAXPROCS 個(gè)自旋的線程 (當(dāng)前例子中的 GOMAXPROCS=4,所以一共 4 個(gè) P),多余的沒事做線程會(huì)讓他們休眠。
3.10 場(chǎng)景十
假定當(dāng)前除了 M3 和 M4 為自旋線程,還有 M5 和 M6 為空閑的線程 (沒有得到 P 的綁定,注意我們這里最多就只能夠存在 4 個(gè) P,所以 P 的數(shù)量應(yīng)該永遠(yuǎn)是 M>=P, 大部分都是 M 在搶占需要運(yùn)行的 P),G8 創(chuàng)建了 G9,G8 進(jìn)行了阻塞的系統(tǒng)調(diào)用,M2 和 P2 立即解綁,P2 會(huì)執(zhí)行以下判斷:如果 P2 本地隊(duì)列有 G、全局隊(duì)列有 G 或有空閑的 M,P2 都會(huì)立馬喚醒 1 個(gè) M 和它綁定,否則 P2 則會(huì)加入到空閑 P 列表,等待 M 來獲取可用的 p。本場(chǎng)景中,P2 本地隊(duì)列有 G9,可以和其他空閑的線程 M5 綁定

3.11 場(chǎng)景十一
G8 創(chuàng)建了 G9,假如 G8 進(jìn)行了非阻塞系統(tǒng)調(diào)用

M2 和 P2 會(huì)解綁,但 M2 會(huì)記住 P2,然后 G8 和 M2 進(jìn)入系統(tǒng)調(diào)用狀態(tài)。當(dāng) G8 和 M2 退出系統(tǒng)調(diào)用時(shí),會(huì)嘗試獲取 P2,如果無法獲取,則獲取空閑的 P,如果依然沒有,G8 會(huì)被記為可運(yùn)行狀態(tài),并加入到全局隊(duì)列,M2 因?yàn)闆]有 P 的綁定而變成休眠狀態(tài) (長時(shí)間休眠等待 GC 回收銷毀)。
到此這篇關(guān)于Golang調(diào)度器GMP原理小結(jié)的文章就介紹到這了,更多相關(guān)Golang調(diào)度器GMP內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
15個(gè)Golang中時(shí)間處理的實(shí)用函數(shù)
在Go編程中,處理日期和時(shí)間是一項(xiàng)常見任務(wù),涉及到精確性和靈活性,本文將介紹一系列實(shí)用函數(shù),它們充當(dāng)time包的包裝器,需要的可以參考下2024-01-01
Golang 經(jīng)典校驗(yàn)庫 validator 用法解析
這篇文章主要為大家介紹了Golang 經(jīng)典校驗(yàn)庫 validator 用法解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
golang開發(fā)?gorilla?websocket的使用示例詳解
這篇文章主要介紹了golang開發(fā)?gorilla?websocket的使用示例詳解,介紹了websocket的簡單使用,我們使用的版本是1.3.0,具體操作方法跟隨小編一起學(xué)習(xí)吧2024-05-05

