溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Go調(diào)度器是如何處理線程阻塞的

發(fā)布時間:2021-11-19 08:11:17 來源:億速云 閱讀:336 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容主要講解“Go調(diào)度器是如何處理線程阻塞的”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Go調(diào)度器是如何處理線程阻塞的”吧!

怎么讓我們的系統(tǒng)更快

隨著信息技術(shù)的迅速發(fā)展,單臺服務(wù)器處理能力越來越強,迫使編程模式由從前的串行模式升級到并發(fā)模型。

并發(fā)模型包含 IO  多路復用、多進程以及多線程,這幾種模型都各有優(yōu)劣,現(xiàn)代復雜的高并發(fā)架構(gòu)大多是幾種模型協(xié)同使用,不同場景應(yīng)用不同模型,揚長避短,發(fā)揮服務(wù)器的最大性能。

而多線程,因為其輕量和易用,成為并發(fā)編程中使用頻率最高的并發(fā)模型,包括后衍生的協(xié)程等其他子產(chǎn)品,也都基于它。

并發(fā) ≠ 并行

并發(fā) (concurrency) 和 并行 ( parallelism) 是不同的。

在單個 CPU 核上,線程通過時間片或者讓出控制權(quán)來實現(xiàn)任務(wù)切換,達到 "同時"  運行多個任務(wù)的目的,這就是所謂的并發(fā)。但實際上任何時刻都只有一個任務(wù)被執(zhí)行,其他任務(wù)通過某種算法來排隊。

多核 CPU 可以讓同一進程內(nèi)的 "多個線程" 做到真正意義上的同時運行,這才是并行。

進程、線程、協(xié)程

進程:進程是系統(tǒng)進行資源分配的基本單位,有獨立的內(nèi)存空間。

線程:線程是 CPU 調(diào)度和分派的基本單位,線程依附于進程存在,每個線程會共享父進程的資源。

協(xié)程:協(xié)程是一種用戶態(tài)的輕量級線程,協(xié)程的調(diào)度完全由用戶控制,協(xié)程間切換只需要保存任務(wù)的上下文,沒有內(nèi)核的開銷。

線程上下文切換

由于中斷處理,多任務(wù)處理,用戶態(tài)切換等原因會導致 CPU 從一個線程切換到另一個線程,切換過程需要保存當前進程的狀態(tài)并恢復另一個進程的狀態(tài)。

上下文切換的代價是高昂的,因為在核心上交換線程會花費很多時間。上下文切換的延遲取決于不同的因素,大概在在 50 到 100  納秒之間??紤]到硬件平均在每個核心上每納秒執(zhí)行 12 條指令,那么一次上下文切換可能會花費 600 到 1200  條指令的延遲時間。實際上,上下文切換占用了大量程序執(zhí)行指令的時間。

如果存在跨核上下文切換(Cross-Core Context Switch),可能會導致 CPU 緩存失效(CPU 從緩存訪問數(shù)據(jù)的成本大約 3 到 40  個時鐘周期,從主存訪問數(shù)據(jù)的成本大約 100 到 300 個時鐘周期),這種場景的切換成本會更加昂貴。

Golang 為并發(fā)而生

Golang 從 2009 年正式發(fā)布以來,依靠其極高運行速度和高效的開發(fā)效率,迅速占據(jù)市場份額。Golang 從語言級別支持并發(fā),通過輕量級協(xié)程  Goroutine 來實現(xiàn)程序并發(fā)運行。

Goroutine 非常輕量,主要體現(xiàn)在以下兩個方面:

上下文切換代價?。?Goroutine 上下文切換只涉及到三個寄存器(PC / SP /  DX)的值修改;而對比線程的上下文切換則需要涉及模式切換(從用戶態(tài)切換到內(nèi)核態(tài))、以及 16 個寄存器、PC、SP…等寄存器的刷新;

內(nèi)存占用少:線程??臻g通常是 2M,Goroutine ??臻g最小 2K;

Golang 程序中可以輕松支持10w 級別的 Goroutine 運行,而線程數(shù)量達到 1k 時,內(nèi)存占用就已經(jīng)達到 2G。

Go 調(diào)度器實現(xiàn)機制:

Go 程序通過調(diào)度器來調(diào)度Goroutine 在內(nèi)核線程上執(zhí)行,但是 Goroutine 并不直接綁定 OS 線程 M - Machine運行,而是由  Goroutine Scheduler 中的 P - Processor (邏輯處理器)來作獲取內(nèi)核線程資源的『中介』。

Go 調(diào)度器模型我們通常叫做G-P-M 模型,他包括 4 個重要結(jié)構(gòu),分別是G、P、M、Sched:

G:Goroutine,每個 Goroutine 對應(yīng)一個 G 結(jié)構(gòu)體,G 存儲 Goroutine 的運行堆棧、狀態(tài)以及任務(wù)函數(shù),可重用。

G 并非執(zhí)行體,每個 G 需要綁定到 P 才能被調(diào)度執(zhí)行。

P: Processor,表示邏輯處理器,對 G 來說,P 相當于 CPU 核,G 只有綁定到 P 才能被調(diào)度。對 M 來說,P  提供了相關(guān)的執(zhí)行環(huán)境(Context),如內(nèi)存分配狀態(tài)(mcache),任務(wù)隊列(G)等。

P 的數(shù)量決定了系統(tǒng)內(nèi)最大可并行的 G 的數(shù)量(前提:物理 CPU 核數(shù) >= P 的數(shù)量)。

P 的數(shù)量由用戶設(shè)置的 GoMAXPROCS 決定,但是不論 GoMAXPROCS 設(shè)置為多大,P 的數(shù)量最大為 256。

M: Machine,OS 內(nèi)核線程抽象,代表著真正執(zhí)行計算的資源,在綁定有效的 P 后,進入 schedule 循環(huán);而 schedule  循環(huán)的機制大致是從 Global 隊列、P 的 Local 隊列以及 wait 隊列中獲取。

M 的數(shù)量是不定的,由 Go Runtime 調(diào)整,為了防止創(chuàng)建過多 OS 線程導致系統(tǒng)調(diào)度不過來,目前默認最大限制為 10000 個。

M 并不保留 G 狀態(tài),這是 G 可以跨 M 調(diào)度的基礎(chǔ)。

Sched:Go 調(diào)度器,它維護有存儲 M 和 G 的隊列以及調(diào)度器的一些狀態(tài)信息等。

調(diào)度器循環(huán)的機制大致是從各種隊列、P 的本地隊列中獲取 G,切換到 G 的執(zhí)行棧上并執(zhí)行 G 的函數(shù),調(diào)用 Goexit 做清理工作并回到  M,如此反復。

理解 M、P、G 三者的關(guān)系,可以通過經(jīng)典的地鼠推車搬磚的模型來說明其三者關(guān)系:

Go調(diào)度器是如何處理線程阻塞的

地鼠(Gopher)的工作任務(wù)是:工地上有若干磚頭,地鼠借助小車把磚頭運送到火種上去燒制。M 就可以看作圖中的地鼠,P 就是小車,G  就是小車里裝的磚。

弄清楚了它們?nèi)叩年P(guān)系,下面我們就開始重點聊地鼠是如何在搬運磚塊的。

Processor(P):

根據(jù)用戶設(shè)置的 GoMAXPROCS 值來創(chuàng)建一批小車(P)。

Goroutine(G):

通過 Go 關(guān)鍵字就是用來創(chuàng)建一個 Goroutine,也就相當于制造一塊磚(G),然后將這塊磚(G)放入當前這輛小車(P)中。

Machine (M):

地鼠(M)不能通過外部創(chuàng)建出來,只能磚(G)太多了,地鼠(M)又太少了,實在忙不過來,剛好還有空閑的小車(P)沒有使用,那就從別處再借些地鼠(M)過來直到把小車(P)用完為止。

這里有一個地鼠(M)不夠用,從別處借地鼠(M)的過程,這個過程就是創(chuàng)建一個內(nèi)核線程(M)。

需要注意的是:地鼠(M) 如果沒有小車(P)是沒辦法運磚的,小車(P)的數(shù)量決定了能夠干活的地鼠(M)數(shù)量,在 Go 程序里面對應(yīng)的是活動線程數(shù);

在 Go 程序里我們通過下面的圖示來展示 G-P-M 模型:

Go調(diào)度器是如何處理線程阻塞的

P 代表可以“并行”運行的邏輯處理器,每個 P 都被分配到一個系統(tǒng)線程 M,G 代表 Go 協(xié)程。

Go 調(diào)度器中有兩個不同的運行隊列:全局運行隊列(GRQ)和本地運行隊列(LRQ)。

每個 P 都有一個 LRQ,用于管理分配給在 P 的上下文中執(zhí)行的 Goroutines,這些 Goroutine 輪流被和 P 綁定的 M  進行上下文切換。GRQ 適用于尚未分配給 P 的 Goroutines。

從上圖可以看出,G 的數(shù)量可以遠遠大于 M 的數(shù)量,換句話說,Go 程序可以利用少量的內(nèi)核級線程來支撐大量 Goroutine 的并發(fā)。多個  Goroutine 通過用戶級別的上下文切換來共享內(nèi)核線程 M 的計算資源,但對于操作系統(tǒng)來說并沒有線程上下文切換產(chǎn)生的性能損耗。

為了更加充分利用線程的計算資源,Go 調(diào)度器采取了以下幾種調(diào)度策略:

任務(wù)竊取(work-stealing)

我們知道,現(xiàn)實情況有的 Goroutine 運行的快,有的慢,那么勢必肯定會帶來的問題就是,忙的忙死,閑的閑死,Go 肯定不允許摸魚的 P  存在,勢必要充分利用好計算資源。

為了提高 Go 并行處理能力,調(diào)高整體處理效率,當每個 P 之間的 G 任務(wù)不均衡時,調(diào)度器允許從 GRQ,或者其他 P 的 LRQ 中獲取 G  執(zhí)行。

減少阻塞

如果正在執(zhí)行的 Goroutine 阻塞了線程 M 怎么辦?P 上 LRQ 中的 Goroutine 會獲取不到調(diào)度么?

在 Go 里面阻塞主要分為一下 4 種場景:

場景 1:由于原子、互斥量或通道操作調(diào)用導致 Goroutine 阻塞,調(diào)度器將把當前阻塞的 Goroutine 切換出去,重新調(diào)度 LRQ 上的其他  Goroutine;

場景 2:由于網(wǎng)絡(luò)請求和 IO 操作導致 Goroutine 阻塞,這種阻塞的情況下,我們的 G 和 M 又會怎么做呢?

Go 程序提供了網(wǎng)絡(luò)輪詢器(NetPoller)來處理網(wǎng)絡(luò)請求和 IO 操作的問題,其后臺通過 kqueue(MacOS),epoll(Linux)或  iocp(Windows)來實現(xiàn) IO 多路復用。

通過使用 NetPoller 進行網(wǎng)絡(luò)系統(tǒng)調(diào)用,調(diào)度器可以防止 Goroutine 在進行這些系統(tǒng)調(diào)用時阻塞 M。這可以讓 M 執(zhí)行 P 的 LRQ  中其他的 Goroutines,而不需要創(chuàng)建新的 M。有助于減少操作系統(tǒng)上的調(diào)度負載。

下圖展示它的工作原理:G1 正在 M 上執(zhí)行,還有 3 個 Goroutine 在 LRQ 上等待執(zhí)行。網(wǎng)絡(luò)輪詢器空閑著,什么都沒干。

Go調(diào)度器是如何處理線程阻塞的

接下來,G1 想要進行網(wǎng)絡(luò)系統(tǒng)調(diào)用,因此它被移動到網(wǎng)絡(luò)輪詢器并且處理異步網(wǎng)絡(luò)系統(tǒng)調(diào)用。然后,M 可以從 LRQ 執(zhí)行另外的 Goroutine。此時,G2  就被上下文切換到 M 上了。

Go調(diào)度器是如何處理線程阻塞的

最后,異步網(wǎng)絡(luò)系統(tǒng)調(diào)用由網(wǎng)絡(luò)輪詢器完成,G1 被移回到 P 的 LRQ 中。一旦 G1 可以在 M 上進行上下文切換,它負責的 Go  相關(guān)代碼就可以再次執(zhí)行。這里的最大優(yōu)勢是,執(zhí)行網(wǎng)絡(luò)系統(tǒng)調(diào)用不需要額外的 M。網(wǎng)絡(luò)輪詢器使用系統(tǒng)線程,它時刻處理一個有效的事件循環(huán)。

Go調(diào)度器是如何處理線程阻塞的

這種調(diào)用方式看起來很復雜,值得慶幸的是,Go 語言將該“復雜性”隱藏在 Runtime 中:Go 開發(fā)者無需關(guān)注 socket 是否是 non-block  的,也無需親自注冊文件描述符的回調(diào),只需在每個連接對應(yīng)的 Goroutine 中以“block I/O”的方式對待 socket 處理即可,實現(xiàn)了  goroutine-per-connection 簡單的網(wǎng)絡(luò)編程模式(但是大量的 Goroutine  也會帶來額外的問題,比如棧內(nèi)存增加和調(diào)度器負擔加重)。

用戶層眼中看到的 Goroutine 中的“block socket”,實際上是通過 Go runtime 中的 netpoller 通過  Non-block socket + I/O 多路復用機制“模擬”出來的。Go 中的 net 庫正是按照這方式實現(xiàn)的。

場景 3:當調(diào)用一些系統(tǒng)方法的時候,如果系統(tǒng)方法調(diào)用的時候發(fā)生阻塞,這種情況下,網(wǎng)絡(luò)輪詢器(NetPoller)無法使用,而進行系統(tǒng)調(diào)用的  Goroutine 將阻塞當前 M。

讓我們來看看同步系統(tǒng)調(diào)用(如文件 I/O)會導致 M 阻塞的情況:G1 將進行同步系統(tǒng)調(diào)用以阻塞 M1。

Go調(diào)度器是如何處理線程阻塞的

調(diào)度器介入后:識別出 G1 已導致 M1 阻塞,此時,調(diào)度器將 M1 與 P 分離,同時也將 G1 帶走。然后調(diào)度器引入新的 M2 來服務(wù)  P。此時,可以從 LRQ 中選擇 G2 并在 M2 上進行上下文切換。

Go調(diào)度器是如何處理線程阻塞的

阻塞的系統(tǒng)調(diào)用完成后:G1 可以移回 LRQ 并再次由 P 執(zhí)行。如果這種情況再次發(fā)生,M1 將被放在旁邊以備將來重復使用。

Go調(diào)度器是如何處理線程阻塞的

場景 4:如果在 Goroutine 去執(zhí)行一個 sleep 操作,導致 M 被阻塞了。

Go 程序后臺有一個監(jiān)控線程 sysmon,它監(jiān)控那些長時間運行的 G 任務(wù)然后設(shè)置可以強占的標識符,別的 Goroutine  就可以搶先進來執(zhí)行。

只要下次這個 Goroutine 進行函數(shù)調(diào)用,那么就會被強占,同時也會保護現(xiàn)場,然后重新放入 P 的本地隊列里面等待下次執(zhí)行。

到此,相信大家對“Go調(diào)度器是如何處理線程阻塞的”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進入相關(guān)頻道進行查詢,關(guān)注我們,繼續(xù)學習!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

go
AI