Go調(diào)度器是如何處理線程阻塞-創(chuàng)新互聯(lián)

今天小編給大家分享的是 Go 程序?yàn)榱藢?shí)現(xiàn)極高的并發(fā)性能,其內(nèi)部調(diào)度器的實(shí)現(xiàn)架構(gòu)(G-P-M 模型),以及為了大限度利用計(jì)算資源,Go 調(diào)度器是如何處理線程阻塞的場(chǎng)景。

福貢網(wǎng)站建設(shè)公司創(chuàng)新互聯(lián),福貢網(wǎng)站設(shè)計(jì)制作,有大型網(wǎng)站制作公司豐富經(jīng)驗(yàn)。已為福貢上千家提供企業(yè)網(wǎng)站建設(shè)服務(wù)。企業(yè)網(wǎng)站搭建\成都外貿(mào)網(wǎng)站制作要多少錢,請(qǐng)找那個(gè)售后服務(wù)好的福貢做網(wǎng)站的公司定做!

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

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

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

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

并發(fā) ≠ 并行

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

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

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

進(jìn)程、線程、協(xié)程

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

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

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

線程上下文切換

由于中斷處理,多任務(wù)處理,用戶態(tài)切換等原因會(huì)導(dǎo)致 CPU 從一個(gè)線程切換到另一個(gè)線程,切換過(guò)程需要保存當(dāng)前進(jìn)程的狀態(tài)并恢復(fù)另一個(gè)進(jìn)程的狀態(tài)。

上下文切換的代價(jià)是高昂的,因?yàn)樵诤诵纳辖粨Q線程會(huì)花費(fèi)很多時(shí)間。上下文切換的延遲取決于不同的因素,大概在在  50  到  100  納秒之間??紤]到硬件平均在每個(gè)核心上每納秒執(zhí)行  12  條指令,那么一次上下文切換可能會(huì)花費(fèi)  600  到  1200  條指令的延遲時(shí)間。實(shí)際上,上下文切換占用了大量程序執(zhí)行指令的時(shí)間。

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

Golang 為并發(fā)而生

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

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

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

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

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

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

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

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

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

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

P: Processor,表示邏輯處理器,對(duì) G 來(lái)說(shuō),P 相當(dāng)于 CPU 核,G 只有綁定到 P 才能被調(diào)度。對(duì) M 來(lái)說(shuō),P 提供了相關(guān)的執(zhí)行環(huán)境(Context),如內(nèi)存分配狀態(tài)(mcache),任務(wù)隊(duì)列(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í)行計(jì)算的資源,在綁定有效的 P 后,進(jìn)入 schedule 循環(huán);而 schedule 循環(huán)的機(jī)制大致是從 Global 隊(duì)列、P 的 Local 隊(duì)列以及 wait 隊(duì)列中獲取。

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

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

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

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

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

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

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

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

Processor(P):

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

Goroutine(G):

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

Machine (M):

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

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

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

在 Go 程序里我們通過(guò)下面的圖示來(lái)展示 G-P-M 模型:

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

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

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

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

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

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

任務(wù)竊?。╳ork-stealing)

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

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

減少阻塞

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

在 Go 里面阻塞主要分為一下 4 種場(chǎng)景:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

小結(jié)

本文主要從 Go 調(diào)度器架構(gòu)層面上介紹了 G-P-M 模型,通過(guò)該模型怎樣實(shí)現(xiàn)少量?jī)?nèi)核線程支撐大量 Goroutine 的并發(fā)運(yùn)行。以及通過(guò) NetPoller、sysmon 等幫助 Go 程序減少線程阻塞,充分利用已有的計(jì)算資源,從而大限度提高 Go 程序的運(yùn)行效率。

看完上文,你是不是對(duì)go了解了更多呢?如果想獲取關(guān)于go的相關(guān)知識(shí),歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊。

分享名稱:Go調(diào)度器是如何處理線程阻塞-創(chuàng)新互聯(lián)
文章路徑:http://muchs.cn/article0/dssooo.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站內(nèi)鏈自適應(yīng)網(wǎng)站、網(wǎng)站排名網(wǎng)站維護(hù)、網(wǎng)站策劃網(wǎng)站收錄

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

小程序開發(fā)