摘要
Go 的調(diào)度機制相當于我們微服務里的基礎(chǔ)組件。很多運行時操作都涉及到了調(diào)度的關(guān)聯(lián)。本文會細聊調(diào)度概念,策略,以及它的機制。當然,也少不了最常提及的 GMP 模型。
一、調(diào)度是什么?
計算機的資源是有限的,像 CPU,內(nèi)存都是固定的。但是同一時間可能會有多個任務要去完成,比如操作系統(tǒng)的定時監(jiān)控,用戶程序的運行等。
怎么讓資源最大化的完成任務,這是調(diào)度需要考慮的關(guān)鍵點。
調(diào)度可以理解為一個指揮員,指導我們的程序按照一定的規(guī)則去獲取資源,然后去執(zhí)行里面的指令。
那么,一般的規(guī)則有哪些呢?
常見的調(diào)度策略有 2 種,一種是協(xié)作式調(diào)度,會讓程序順利的完成自己的任務,再把資源騰出來給其他程序使用。
另一種是搶占式調(diào)度,也就是讓程序按一定的時間去占有這些資源,時間到了就被迫讓出現(xiàn)有資源,給其他的程序輪流使用。
協(xié)作式調(diào)度有利于程序?qū)W⒌耐瓿勺约旱娜蝿?,但也可能會造成其他程序一?strong>餓死,得不到執(zhí)行。
搶占式調(diào)度有利于程序在資源的利用上雨露均沾,但是在不斷的切換過程中,將會使得程序原本 10 ms 能完成的事,不得不延遲多幾 ms。
注:Linux 操作系統(tǒng)也是采用了搶占式調(diào)度,并且使用了 CFS:完全公平調(diào)度算法。通過對程序大致的運行時間來平衡調(diào)度,讓越?jīng)]有執(zhí)行過的程序,越快被調(diào)度到。
當前大多數(shù)操作系統(tǒng)都是采用搶占式調(diào)度來執(zhí)行程序的,畢竟很多操作系統(tǒng)都是面向用戶,需要很高的響應速度,而且只要切換程序的周期夠短,例如 50ms,那對于用戶來講,就像沒切換一樣。
二、golang 的調(diào)度
上面提及到搶占式調(diào)度會有個頻繁切換的過程,在切換時,需要不斷的保存或恢復上下文信息。
而這會涉及到操作系統(tǒng)內(nèi)核態(tài)和用戶態(tài)的切換,性能損耗會很大。
對此,golang 實現(xiàn)了屬于自己的調(diào)度模型,采用了基于協(xié)作的搶占式調(diào)度。之所以是"協(xié)作"的,是因為 Go 的調(diào)度時機是由用戶自己設(shè)置的,而這里的用戶指的是 golang 的運行時 runtime。
它會在下面的事件發(fā)生時進行調(diào)度觸發(fā):
- 使用關(guān)鍵字 go
- 垃圾回收
- 系統(tǒng)調(diào)用,如訪問硬盤
- 同步阻塞調(diào)用,如 使用 mutex、channel
如果上面什么事件都沒發(fā)生,則會有 sysmon 來監(jiān)控 goroutine 的運行情況,對長時間運行的 goroutine 進行標記。一旦 goroutine 被標記了,那么它就會下次發(fā)生函數(shù)調(diào)用時,將自己掛起,再觸發(fā)調(diào)度。
這里需要說明下的是,runtime 它相當于 Java 的虛擬機,負責了 Go 的很多東西,例如調(diào)度,垃圾回收、內(nèi)存管理等,可以說是涵蓋了 Go 的基礎(chǔ)引擎了。
更重要的是 runtime 是運行在用戶態(tài)上的,相當于 Go 的調(diào)度是在用戶態(tài)這一層進行的。
這樣,每當 Go 有調(diào)度產(chǎn)生時,就不會伴隨著用戶態(tài)和內(nèi)核態(tài)的切換,而是像前面提到過的策略那樣去觸發(fā)調(diào)度,這就降低了并發(fā)時的內(nèi)核態(tài)與用戶態(tài)的切換成本了。
三、golang 的 GPM 模型
為了實現(xiàn) golang 的調(diào)度,golang 抽象出了三個結(jié)構(gòu),也就是我們常見的 G、P、M。
G:也就是協(xié)程 goroutine,由 Go runtime 管理。我們可以認為它是用戶級別的線程。
goroutine 非常的輕量,初始分配只有 2KB,當??臻g不夠用時,會自動擴容。同時,自身存儲了執(zhí)行 stack 信息、goroutine 狀態(tài)以及 goroutine 的任務函數(shù)等。
P:processor 處理器。P 的數(shù)量默認跟 CPU 的核心數(shù)一樣,如果是多核的 CPU,則會有多個 P 會被創(chuàng)建。
每當有 goroutine 要創(chuàng)建時,會被添加到 P 上的 goroutine 本地隊列上,如果 P 的本地隊列已滿,則會維護到全局隊列里。
在進行調(diào)度時,會優(yōu)先從本地隊列獲取 goroutine 來執(zhí)行。
如果本地隊列沒有,會從其他的 P 上偷取 goroutine。
如果其他 P 上也沒有,則會從全局隊列上獲取 goroutine。
這樣通過上面的策略,就能盡最大努力保證有 goroutine 可運行。
M:系統(tǒng)線程。在 M 上有調(diào)度函數(shù),它是真正的調(diào)度執(zhí)行者,M 需要跟 P 綁定,并且會讓 P 按上面的原則挑出個 goroutine 來執(zhí)行。
M 雖然從 P 上挑選了 G 執(zhí)行,但 M 并不保存 G 的上下文信息,而是 G 自己保存了相關(guān)信息,這樣有利于轉(zhuǎn)移到其他 M 上,在不同的 M 上運行。
GPM 模型的優(yōu)勢點在于 G 包含了執(zhí)行任務相關(guān)信息,M 提供了執(zhí)行環(huán)境,并且有調(diào)度機制。而 P 則是他們兩者的粘合劑。
假如沒有 P 。那么 M 就會有爭奪 G 的競爭問題,并且 M 的數(shù)量會不可控,會出現(xiàn)過多的 M 去處理 G。
一旦超過了 CPU 的核心數(shù),那么就會將性能耗費在上下文切換過程中。
有了 P 這一層后,M 優(yōu)先從 P 的本地隊列獲取 goroutine,減少并發(fā)競爭。并且保證了最多跟 CPU 核心數(shù)一樣的 goroutine 數(shù)量在并行運行,充分利用了多核優(yōu)勢,又不被濫用。
總結(jié)
相信看過本文后,各位對 Golang 的調(diào)度有了一定的了解。正是因為基于協(xié)作的搶占式調(diào)度和 GMP 模型,Golang 的高并發(fā)高性能才有了底層保障。當然,大伙也可以深入到源碼去分析這些調(diào)度機制,這樣離大神就更近一步了 ?...