摘要
Go 的調(diào)度機(jī)制相當(dāng)于我們微服務(wù)里的基礎(chǔ)組件。很多運(yùn)行時(shí)操作都涉及到了調(diào)度的關(guān)聯(lián)。本文會(huì)細(xì)聊調(diào)度概念,策略,以及它的機(jī)制。當(dāng)然,也少不了最常提及的 GMP 模型。
一拳话、調(diào)度是什么?
計(jì)算機(jī)的資源是有限的,像 CPU种吸,內(nèi)存都是固定的坚俗。但是同一時(shí)間可能會(huì)有多個(gè)任務(wù)要去完成速缆,比如操作系統(tǒng)的定時(shí)監(jiān)控送滞,用戶程序的運(yùn)行等褂微。
怎么讓資源最大化的完成任務(wù),這是調(diào)度需要考慮的關(guān)鍵點(diǎn)园爷。
調(diào)度可以理解為一個(gè)指揮員求厕,指導(dǎo)我們的程序按照一定的規(guī)則去獲取資源,然后去執(zhí)行里面的指令扰楼。
那么呀癣,一般的規(guī)則有哪些呢?
常見的調(diào)度策略有 2 種弦赖,一種是協(xié)作式調(diào)度项栏,會(huì)讓程序順利的完成自己的任務(wù),再把資源騰出來給其他程序使用蹬竖。
另一種是搶占式調(diào)度沼沈,也就是讓程序按一定的時(shí)間去占有這些資源,時(shí)間到了就被迫讓出現(xiàn)有資源币厕,給其他的程序輪流使用列另。
協(xié)作式調(diào)度有利于程序?qū)W⒌耐瓿勺约旱娜蝿?wù),但也可能會(huì)造成其他程序一直餓死旦装,得不到執(zhí)行访递。
搶占式調(diào)度有利于程序在資源的利用上雨露均沾,但是在不斷的切換過程中同辣,將會(huì)使得程序原本 10 ms 能完成的事拷姿,不得不延遲多幾 ms。
注:Linux 操作系統(tǒng)也是采用了搶占式調(diào)度旱函,并且使用了 CFS:完全公平調(diào)度算法响巢。通過對程序大致的運(yùn)行時(shí)間來平衡調(diào)度,讓越?jīng)]有執(zhí)行過的程序棒妨,越快被調(diào)度到踪古。
當(dāng)前大多數(shù)操作系統(tǒng)都是采用搶占式調(diào)度來執(zhí)行程序的含长,畢竟很多操作系統(tǒng)都是面向用戶,需要很高的響應(yīng)速度伏穆,而且只要切換程序的周期夠短拘泞,例如 50ms,那對于用戶來講枕扫,就像沒切換一樣陪腌。
二、golang 的調(diào)度
上面提及到搶占式調(diào)度會(huì)有個(gè)頻繁切換的過程烟瞧,在切換時(shí)诗鸭,需要不斷的保存或恢復(fù)上下文信息。
而這會(huì)涉及到操作系統(tǒng)內(nèi)核態(tài)和用戶態(tài)的切換参滴,性能損耗會(huì)很大强岸。
對此,golang 實(shí)現(xiàn)了屬于自己的調(diào)度模型砾赔,采用了基于協(xié)作的搶占式調(diào)度蝌箍。之所以是"協(xié)作"的,是因?yàn)?Go 的調(diào)度時(shí)機(jī)是由用戶自己設(shè)置的暴心,而這里的用戶指的是 golang 的運(yùn)行時(shí) runtime妓盲。
它會(huì)在下面的事件發(fā)生時(shí)進(jìn)行調(diào)度觸發(fā):
- 使用關(guān)鍵字 go
- 垃圾回收
- 系統(tǒng)調(diào)用,如訪問硬盤
- 同步阻塞調(diào)用酷勺,如 使用 mutex本橙、channel
如果上面什么事件都沒發(fā)生扳躬,則會(huì)有 sysmon 來監(jiān)控 goroutine 的運(yùn)行情況脆诉,對長時(shí)間運(yùn)行的 goroutine 進(jìn)行標(biāo)記。一旦 goroutine 被標(biāo)記了贷币,那么它就會(huì)下次發(fā)生函數(shù)調(diào)用時(shí)击胜,將自己掛起,再觸發(fā)調(diào)度检号。
這里需要說明下的是葵袭,runtime 它相當(dāng)于 Java 的虛擬機(jī)届氢,負(fù)責(zé)了 Go 的很多東西,例如調(diào)度辰斋,垃圾回收、內(nèi)存管理等瘸味,可以說是涵蓋了 Go 的基礎(chǔ)引擎了宫仗。
更重要的是 runtime 是運(yùn)行在用戶態(tài)上的,相當(dāng)于 Go 的調(diào)度是在用戶態(tài)這一層進(jìn)行的旁仿。
這樣藕夫,每當(dāng) Go 有調(diào)度產(chǎn)生時(shí),就不會(huì)伴隨著用戶態(tài)和內(nèi)核態(tài)的切換,而是像前面提到過的策略那樣去觸發(fā)調(diào)度毅贮,這就降低了并發(fā)時(shí)的內(nèi)核態(tài)與用戶態(tài)的切換成本了办悟。
三、golang 的 GPM 模型
為了實(shí)現(xiàn) golang 的調(diào)度滩褥,golang 抽象出了三個(gè)結(jié)構(gòu)病蛉,也就是我們常見的 G、P铸题、M铡恕。
G:也就是協(xié)程 goroutine,由 Go runtime 管理丢间。我們可以認(rèn)為它是用戶級別的線程探熔。
goroutine 非常的輕量,初始分配只有 2KB烘挫,當(dāng)椌骷瑁空間不夠用時(shí),會(huì)自動(dòng)擴(kuò)容饮六。同時(shí)其垄,自身存儲(chǔ)了執(zhí)行 stack 信息、goroutine 狀態(tài)以及 goroutine 的任務(wù)函數(shù)等卤橄。
P:processor 處理器绿满。P 的數(shù)量默認(rèn)跟 CPU 的核心數(shù)一樣,如果是多核的 CPU窟扑,則會(huì)有多個(gè) P 會(huì)被創(chuàng)建喇颁。
每當(dāng)有 goroutine 要?jiǎng)?chuàng)建時(shí),會(huì)被添加到 P 上的 goroutine 本地隊(duì)列上嚎货,如果 P 的本地隊(duì)列已滿橘霎,則會(huì)維護(hù)到全局隊(duì)列里。
在進(jìn)行調(diào)度時(shí)殖属,會(huì)優(yōu)先從本地隊(duì)列獲取 goroutine 來執(zhí)行姐叁。
如果本地隊(duì)列沒有,會(huì)從其他的 P 上偷取 goroutine洗显。
如果其他 P 上也沒有外潜,則會(huì)從全局隊(duì)列上獲取 goroutine。
這樣通過上面的策略挠唆,就能盡最大努力保證有 goroutine 可運(yùn)行处窥。
M:系統(tǒng)線程。在 M 上有調(diào)度函數(shù)损搬,它是真正的調(diào)度執(zhí)行者碧库,M 需要跟 P 綁定柜与,并且會(huì)讓 P 按上面的原則挑出個(gè) goroutine 來執(zhí)行。
M 雖然從 P 上挑選了 G 執(zhí)行嵌灰,但 M 并不保存 G 的上下文信息弄匕,而是 G 自己保存了相關(guān)信息,這樣有利于轉(zhuǎn)移到其他 M 上沽瞭,在不同的 M 上運(yùn)行迁匠。
GPM 模型的優(yōu)勢點(diǎn)在于 G 包含了執(zhí)行任務(wù)相關(guān)信息,M 提供了執(zhí)行環(huán)境驹溃,并且有調(diào)度機(jī)制城丧。而 P 則是他們兩者的粘合劑。
假如沒有 P 豌鹤。那么 M 就會(huì)有爭奪 G 的競爭問題亡哄,并且 M 的數(shù)量會(huì)不可控,會(huì)出現(xiàn)過多的 M 去處理 G布疙。
一旦超過了 CPU 的核心數(shù)蚊惯,那么就會(huì)將性能耗費(fèi)在上下文切換過程中。
有了 P 這一層后灵临,M 優(yōu)先從 P 的本地隊(duì)列獲取 goroutine截型,減少并發(fā)競爭。并且保證了最多跟 CPU 核心數(shù)一樣的 goroutine 數(shù)量在并行運(yùn)行儒溉,充分利用了多核優(yōu)勢宦焦,又不被濫用。
總結(jié)
相信看過本文后顿涣,各位對 Golang 的調(diào)度有了一定的了解波闹。正是因?yàn)榛趨f(xié)作的搶占式調(diào)度和 GMP 模型,Golang 的高并發(fā)高性能才有了底層保障园骆。當(dāng)然舔痪,大伙也可以深入到源碼去分析這些調(diào)度機(jī)制寓调,這樣離大神就更近一步了 ?...