揭秘系列:Goroutine調(diào)度器
現(xiàn)在不要擔(dān)心理解上面的圖片,因?yàn)槲覀儗姆浅;A(chǔ)的知識(shí)開始。
Goroutines分布在線程中,由Goroutine調(diào)度器在幕后處理。根據(jù)我們之前的討論,我們知道一些關(guān)于Goroutines的事情:
- 從原始執(zhí)行速度來看,Goroutines不一定比線程更快,因?yàn)樗鼈冃枰粋€(gè)實(shí)際的線程來運(yùn)行。
- Goroutines的真正優(yōu)勢(shì)在于上下文切換、內(nèi)存占用、創(chuàng)建和拆除的成本等方面。
你可能之前聽說過Goroutine調(diào)度器,但我們真正了解它是如何工作的嗎?它是如何將Goroutines與線程配對(duì)的?
現(xiàn)在讓我們一步一步地分解調(diào)度器的操作。
一、Goroutine的M:N調(diào)度器
Go團(tuán)隊(duì)為我們真正簡(jiǎn)化了并發(fā)處理,想想看:創(chuàng)建一個(gè)Goroutine就像在函數(shù)前面加上 go 關(guān)鍵字一樣容易。
go doWork()
但在這個(gè)簡(jiǎn)單的步驟背后,有一個(gè)更深層的系統(tǒng)在運(yùn)作。
從一開始,Go并不是簡(jiǎn)單地提供了線程。相反,在中間有一個(gè)輔助工具,Goroutine調(diào)度器,它是Go運(yùn)行時(shí)的關(guān)鍵部分。
1*6UyqGhkbOV7kSlRe1CD-gA.png
那么什么是M:N標(biāo)簽?
它表示Go調(diào)度器在將M個(gè)Goroutines映射到N個(gè)內(nèi)核線程時(shí)所起的作用,形成了M:N模型。你可以擁有更多的操作系統(tǒng)線程,就像可以擁有更多的Goroutines一樣。
在我們深入研究調(diào)度器之前,讓我們澄清一下經(jīng)常混淆的兩個(gè)術(shù)語:并發(fā)和并行。
- 并發(fā):這是關(guān)于同時(shí)處理多個(gè)任務(wù),它們都在運(yùn)動(dòng),但不總是在同一時(shí)刻。
- 并行:這意味著許多任務(wù)在完全相同的時(shí)間運(yùn)行,通常使用多個(gè)CPU核心。
1*30ViMAPkVySvdSDc-hI3sA.png
讓我們看看Go調(diào)度器是如何與線程配合運(yùn)作的。
二、PMG 模型
在我們深入研究?jī)?nèi)部工作原理之前,讓我們分解一下P、M和G代表什么。
1.G(Goroutine)
Goroutine充當(dāng)Go的最小執(zhí)行單元,類似于輕量級(jí)線程。
在Go的運(yùn)行時(shí)中,它由一個(gè)名為g的struct{}表示。一旦創(chuàng)建,它會(huì)找到一個(gè)邏輯處理器P的本地可運(yùn)行隊(duì)列(或G隊(duì)列)中的位置,然后P將其交給一個(gè)實(shí)際的內(nèi)核線程M。
Goroutines通常存在三種主要狀態(tài):
- 等待:在這個(gè)階段,Goroutine停滯不前,可能因?yàn)橥ǖ阑蜴i之類的操作而暫停,或者可能被系統(tǒng)調(diào)用暫停。
- 可運(yùn)行:Goroutine已經(jīng)準(zhǔn)備好運(yùn)行,但尚未開始運(yùn)行,它正在等待輪到它在一個(gè)線程(M)上運(yùn)行。
- 運(yùn)行:現(xiàn)在,Goroutine正在一個(gè)線程(M)上積極執(zhí)行。它會(huì)一直執(zhí)行,直到任務(wù)完成,除非調(diào)度器中斷它或其他原因阻止了它的執(zhí)行。
1*U62eyES6_koQtsv_9jWKHw.png
Goroutines并不僅僅被使用一次然后被丟棄。
相反,當(dāng)啟動(dòng)新的Goroutine時(shí),Go的運(yùn)行時(shí)會(huì)從Goroutine池
中選擇一個(gè),但如果找不到任何可用的Goroutine,它會(huì)創(chuàng)建一個(gè)新的。然后,這個(gè)新的Goroutine加入到一個(gè)P的可運(yùn)行隊(duì)列中。
2.P(邏輯處理器)
在Go調(diào)度器中,當(dāng)我們提到“處理器”時(shí),我們指的是邏輯實(shí)體,而不是物理實(shí)體。
默認(rèn)情況下,P的數(shù)量設(shè)置為可用的核心數(shù),你可以使用runtime.GOMAXPROCS(int)函數(shù)來檢查或更改這些處理器的數(shù)量。
runtime.GOMAXPROCS(0) // 獲取當(dāng)前允許的邏輯處理器數(shù)量
如果你想更改這個(gè)數(shù)量,最好是在應(yīng)用程序啟動(dòng)時(shí)更改它,如果在運(yùn)行時(shí)更改,它會(huì)導(dǎo)致STW(停止一切),直到重新調(diào)整處理器。
每個(gè)P都擁有自己的可運(yùn)行Goroutines列表,稱為本地運(yùn)行隊(duì)列,最多可以容納256個(gè)Goroutines。
1*0EneA397HA1uYYeg0_HspQ.png
調(diào)度器 — P(邏輯處理器)
如果P的隊(duì)列達(dá)到了最大Coroutines數(shù)(256),那么就有一個(gè)共享隊(duì)列,稱為全局運(yùn)行隊(duì)列,但我們將稍后討論這個(gè)。
"那么 'P' 的這個(gè)數(shù)量到底代表什么?" 它表示可以同時(shí)運(yùn)行的Goroutines數(shù)量 — 想象它們并行運(yùn)行。
3.M(機(jī)器線程 — 操作系統(tǒng)線程)
一個(gè)典型的Go程序最多可以使用1萬個(gè)線程。
是的,我說的是線程,而不是Goroutines。如果超出這個(gè)限制,你可能會(huì)使你的Go應(yīng)用程序崩潰。
"什么情況下會(huì)創(chuàng)建一個(gè)線程?" 想象一種情況:一個(gè)Goroutine處于可運(yùn)行狀態(tài)并需要一個(gè)線程。如果所有線程已經(jīng)被阻塞,可能是因?yàn)橄到y(tǒng)調(diào)用或非搶占操作,會(huì)怎么樣?在這種情況下,調(diào)度器會(huì)介入并為該Goroutine創(chuàng)建一個(gè)新線程。一個(gè)需要注意的事情是,如果一個(gè)線程只是忙于昂貴的計(jì)算或長時(shí)間運(yùn)行的任務(wù),它不被視為被卡住或被阻塞。如果你想更改默認(rèn)線程限制,你可以使用 runtime/debug.SetMaxThreads() 函數(shù),它允許你設(shè)置你的Go程序可以使用的操作系統(tǒng)線程的最大數(shù)量。此外,值得知道的是,線程是可以重復(fù)使用的,因?yàn)閯?chuàng)建或銷毀線程是消耗資源的。
三、MPG 工作原理
讓我們通過項(xiàng)目符號(hào)逐步了解 M、P 和 G 如何一起運(yùn)作。
我不會(huì)在這里深入討論每個(gè)細(xì)節(jié),但我將在即將發(fā)布的故事中深入探討。如果你感興趣,請(qǐng)訂閱。
1*d4hu416FJtHHaJaKJFYkGg.png
1.Go Scheduler 的工作原理:
- 初始化 goroutine:通過使用 go func() 命令,Go Runtime 要么創(chuàng)建一個(gè)新的 goroutine,要么從池中選擇一個(gè)現(xiàn)有的。
- 排隊(duì)位置:goroutine 尋找其在隊(duì)列中的位置,如果所有邏輯處理器(P)的本地隊(duì)列都滿了,那么這個(gè) goroutine 就被放入全局隊(duì)列。
- 線程配對(duì):這是 M 開始發(fā)揮作用的地方。它獲取一個(gè) P 并開始處理來自 P 本地隊(duì)列的 goroutine,當(dāng) M 與這個(gè) goroutine 交互時(shí),與之關(guān)聯(lián)的 P 就變得占用,不再可用于其他 M。
- 竊取行為:如果某個(gè) P 的隊(duì)列被耗盡,M 會(huì)嘗試“借用”另一個(gè) P 隊(duì)列中一半可運(yùn)行的 goroutine。如果不成功,它然后檢查全局隊(duì)列,然后再檢查網(wǎng)絡(luò)輪詢器(請(qǐng)查看下面的“竊取過程”圖表部分)。
- 資源分配:M 選擇了一個(gè) goroutine(G)之后,它會(huì)獲取運(yùn)行 G 所需的所有資源。
“那么被阻塞的線程呢?” 如果一個(gè) goroutine 啟動(dòng)了需要時(shí)間的系統(tǒng)調(diào)用(比如讀取文件),那么 M 會(huì)等待。但調(diào)度程序不喜歡某個(gè)只是坐在那里等待的線程,它會(huì)將被暫停的 M 與其 P 解除連接,并將來自隊(duì)列的另一個(gè)可運(yùn)行的 goroutine 與新的或現(xiàn)有的 M 連接起來,然后與 P 協(xié)作。
2.被阻塞的線程
竊取過程:
當(dāng)一個(gè)線程(M)完成了它的任務(wù)并沒有其他事情可做時(shí),它不會(huì)坐在那里。
相反,它積極地尋找更多工作,觀察其他處理器并獲取它們一半的任務(wù),讓我們來詳細(xì)了解一下:
ewA.png
- 每 61 個(gè)時(shí)鐘滴答,M 檢查全局可運(yùn)行隊(duì)列,以確保執(zhí)行的公平性。如果在全局隊(duì)列中找到一個(gè)可運(yùn)行的 goroutine,就停止。
- 然后,線程 M 檢查其本地運(yùn)行隊(duì)列,與其處理器 P 相關(guān)聯(lián),以查看是否有可運(yùn)行的 goroutine 可以處理。
- 如果線程發(fā)現(xiàn)它的隊(duì)列是空的,那么它會(huì)查看全局隊(duì)列,看看那里是否有等待處理的任務(wù)。
- 然后,線程會(huì)檢查網(wǎng)絡(luò)輪詢器,以查看是否有與網(wǎng)絡(luò)相關(guān)的任務(wù)。
- 如果線程在檢查了網(wǎng)絡(luò)輪詢器后仍然沒有找到任務(wù),它將進(jìn)入主動(dòng)搜索模式,我們可以將其視為旋轉(zhuǎn)狀態(tài)。
- 在這種狀態(tài)下,線程試圖從其他處理器的隊(duì)列中“借用”任務(wù)。
- 經(jīng)過所有這些步驟后,如果線程仍然找不到工作,它將停止主動(dòng)搜索。
- 現(xiàn)在,如果有新的任務(wù)進(jìn)來,而且有一個(gè)沒有在搜索狀態(tài)的空閑處理器,那么可以提示另一個(gè)線程開始工作。
需要注意的細(xì)節(jié)是全局隊(duì)列實(shí)際上被檢查了兩次:每 61 個(gè)時(shí)鐘滴答一次以確保公平性,如果本地隊(duì)列為空,就再次檢查。
“如果 M 與其 P 相關(guān)聯(lián),它怎么能從其他處理器那里獲取任務(wù)呢?M 會(huì)更改其 P 嗎?” 答案是不會(huì)。即使 M 從另一個(gè) P 的隊(duì)列中獲取任務(wù),它仍然使用其原始處理器來運(yùn)行該任務(wù)。因此,在 M 承擔(dān)新任務(wù)的同時(shí),它仍然忠實(shí)于其處理器。
“為什么是 61?” 在設(shè)計(jì)算法時(shí),特別是哈希算法,通常會(huì)選擇質(zhì)數(shù),因?yàn)樗鼈兂?1 和它們自己之外沒有除數(shù)。這可以降低出現(xiàn)模式或規(guī)律的機(jī)會(huì),從而防止“碰撞”或其他不希望出現(xiàn)的行為。如果太短,系統(tǒng)可能會(huì)浪費(fèi)資源頻繁檢查全局運(yùn)行隊(duì)列。如果太長,goroutine 可能會(huì)在執(zhí)行之前等待過長的時(shí)間。
3.網(wǎng)絡(luò)輪詢器
我們還沒有詳細(xì)討論網(wǎng)絡(luò)輪詢器,但它在竊取過程圖表中提到了。
與 Go Scheduler 一樣,網(wǎng)絡(luò)輪詢器是 Go Runtime 的組成部分,負(fù)責(zé)處理與網(wǎng)絡(luò)相關(guān)的調(diào)用(例如,網(wǎng)絡(luò) I/O)。
讓我們比較兩種系統(tǒng)調(diào)用類型:
- 與網(wǎng)絡(luò)相關(guān)的系統(tǒng)調(diào)用:當(dāng)一個(gè) goroutine 執(zhí)行網(wǎng)絡(luò) I/O 操作時(shí),它不會(huì)阻塞線程,而是會(huì)在網(wǎng)絡(luò)輪詢器中注冊(cè)。輪詢器會(huì)異步等待操作完成,一旦完成,goroutine 就會(huì)再次可運(yùn)行,可以在一個(gè)線程上繼續(xù)執(zhí)行。
- 其他系統(tǒng)調(diào)用:如果它們可能會(huì)阻塞并且不由網(wǎng)絡(luò)輪詢器處理,它們可能會(huì)導(dǎo)致 goroutine 將其執(zhí)行卸載到操作系統(tǒng)線程上。只有特定的操作系統(tǒng)線程會(huì)被阻塞,Go 運(yùn)行時(shí)調(diào)度程序可以在不同線程上執(zhí)行其他 goroutine。