您好,登錄后才能下訂單哦!
今天小編給大家分享的是 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)景。
怎么讓我們的系統(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)系:
地鼠(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 模型:
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)干。
接下來(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 上了。
最后,異步網(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)。
這種調(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。
調(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)行上下文切換。
阻塞的系統(tǒng)調(diào)用完成后:G1 可以移回 LRQ 并再次由 P 執(zhí)行。如果這種情況再次發(fā)生,M1 將被放在旁邊以備將來(lái)重復(fù)使用。
場(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)注億速云行業(yè)資訊。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。