你覺(jué)得 Go 在什么時(shí)候會(huì)搶占 P?
在 Go 語(yǔ)言中,Goroutine 是并發(fā)模型的核心,而 P(Processor) 是 Go 調(diào)度器中的一個(gè)關(guān)鍵抽象。理解 Goroutine 調(diào)度模型 中的 G(Goroutine)、M(Machine,內(nèi)核線程)、P(Processor,邏輯處理器) 的關(guān)系可以幫助我們理解 Go 的搶占式調(diào)度策略。
Go 調(diào)度器使用 G-M-P 模型:
- Goroutine (G):一個(gè) Goroutine 代表一個(gè) Go 協(xié)程。
- Processor (P):P 是邏輯處理器,負(fù)責(zé)調(diào)度和管理 Goroutine,最多有 GOMAXPROCS 個(gè) P。每個(gè) P 可以運(yùn)行一個(gè) Goroutine。
- Machine (M):M 是操作系統(tǒng)的內(nèi)核線程。每個(gè) M 需要綁定一個(gè) P 來(lái)執(zhí)行 Goroutine。
#Go 中的搶占式調(diào)度
Go 的調(diào)度器采用的是協(xié)作式調(diào)度為主,搶占式調(diào)度為輔。協(xié)作式調(diào)度意味著 Goroutine 需要主動(dòng)放棄控制權(quán)來(lái)讓其他 Goroutine 運(yùn)行,比如調(diào)用系統(tǒng)調(diào)用或者 Goroutine 自己調(diào)用 runtime.Gosched()。
搶占式調(diào)度則是為了防止某些 Goroutine 占用 CPU 太久(比如某個(gè) Goroutine 在長(zhǎng)時(shí)間執(zhí)行計(jì)算密集型任務(wù)),Go 1.14 引入了針對(duì) 計(jì)算密集型 Goroutine 的 搶占式調(diào)度。搶占式調(diào)度可以在以下場(chǎng)景下觸發(fā):
- Goroutine 執(zhí)行時(shí)間過(guò)長(zhǎng),特別是沒(méi)有主動(dòng)進(jìn)行系統(tǒng)調(diào)用、調(diào)度讓出等行為時(shí)。
- Goroutine 執(zhí)行在較長(zhǎng)的函數(shù)調(diào)用鏈上,或者在一些函數(shù)的棧幀擴(kuò)展時(shí)(例如深度遞歸調(diào)用或大數(shù)組操作時(shí))。
#搶占 P 的時(shí)機(jī)
- 系統(tǒng)調(diào)用 (syscall) 后:當(dāng) Goroutine 執(zhí)行系統(tǒng)調(diào)用后,Goroutine 會(huì)讓出 P,此時(shí)調(diào)度器可能會(huì)選擇調(diào)度其他的 Goroutine 來(lái)運(yùn)行。
- 垃圾回收 (GC) 階段:當(dāng)觸發(fā)垃圾回收時(shí),調(diào)度器會(huì)在合適時(shí)機(jī)搶占 Goroutine,確保 GC 可以進(jìn)行。
- 計(jì)算密集型任務(wù)被長(zhǎng)時(shí)間運(yùn)行:從 Go 1.14 開始,調(diào)度器會(huì)定期檢查長(zhǎng)時(shí)間運(yùn)行的 Goroutine,并進(jìn)行搶占。
#搶占式調(diào)度與長(zhǎng)時(shí)間運(yùn)行的 Goroutine
下面的例子展示了一個(gè) Goroutine 在執(zhí)行計(jì)算密集型任務(wù)時(shí)如何可能會(huì)被 Go 的搶占式調(diào)度機(jī)制打斷。
package main
import (
"fmt"
"runtime"
"time"
)
// 模擬一個(gè)計(jì)算密集型任務(wù)
func busyLoop() {
for i := 0; i < 1e10; i++ {
// 占用 CPU,但沒(méi)有主動(dòng)讓出調(diào)度權(quán)
}
fmt.Println("Finished busy loop")
}
func main() {
runtime.GOMAXPROCS(1) // 設(shè)置只有 1 個(gè) P
go func() {
for {
fmt.Println("Running another goroutine...")
time.Sleep(500 * time.Millisecond) // 每 500 毫秒休息一次
}
}()
busyLoop() // 執(zhí)行計(jì)算密集型任務(wù)
time.Sleep(2 * time.Second)
}
#代碼解析:
- runtime.GOMAXPROCS(1):我們將 GOMAXPROCS 設(shè)置為 1,意味著整個(gè)程序中只有一個(gè) P,這樣所有 Goroutine 都只能在這個(gè) P 上調(diào)度。
- busyLoop:這是一個(gè)計(jì)算密集型任務(wù),在沒(méi)有主動(dòng)進(jìn)行系統(tǒng)調(diào)用或讓出調(diào)度權(quán)的情況下,循環(huán)執(zhí)行大量的操作,耗盡 CPU 時(shí)間。
- 搶占:雖然 busyLoop 沒(méi)有主動(dòng)讓出 CPU,但由于 Go 的搶占式調(diào)度機(jī)制,調(diào)度器可能會(huì)在合適的時(shí)間點(diǎn)打斷 busyLoop,讓其他 Goroutine(比如打印 "Running another goroutine..." 的那個(gè) Goroutine)得到執(zhí)行機(jī)會(huì)。
#輸出示例:
Running another goroutine...
Running another goroutine...
...
Finished busy loop
我們可以看到,盡管 busyLoop 是一個(gè)計(jì)算密集型任務(wù),其他的 Goroutine 仍然會(huì)間歇性地被調(diào)度并執(zhí)行。這個(gè)就是 Go 搶占式調(diào)度的效果。
#搶占的實(shí)現(xiàn)機(jī)制
搶占式調(diào)度的核心機(jī)制是 定期檢查 Goroutine 的執(zhí)行時(shí)間。Go 調(diào)度器在后臺(tái)維護(hù)一個(gè)時(shí)間戳,記錄 Goroutine 上次被調(diào)度的時(shí)間。調(diào)度器每隔一段時(shí)間會(huì)檢查當(dāng)前運(yùn)行的 Goroutine,如果 Goroutine 占用了 CPU 超過(guò)一定時(shí)間,調(diào)度器就會(huì)標(biāo)記這個(gè) Goroutine 需要被搶占,然后調(diào)度其他的 Goroutine 來(lái)執(zhí)行。
搶占式調(diào)度通過(guò)以下方式觸發(fā):
- 函數(shù)調(diào)用邊界:當(dāng) Goroutine 進(jìn)行函數(shù)調(diào)用時(shí),Go runtime 會(huì)在合適的時(shí)機(jī)插入搶占檢查點(diǎn)。
- 棧增長(zhǎng):當(dāng) Goroutine 的棧增長(zhǎng)(如遞歸調(diào)用導(dǎo)致棧內(nèi)存增長(zhǎng))時(shí),調(diào)度器也會(huì)插入搶占檢查。
- GC 安全點(diǎn):垃圾回收過(guò)程中,調(diào)度器也會(huì)嘗試搶占。
#通過(guò)代碼觀察搶占效果
我們可以通過(guò)使用 GODEBUG 環(huán)境變量,啟用搶占式調(diào)度的調(diào)試日志,觀察搶占調(diào)度的具體行為。運(yùn)行如下代碼時(shí),啟用調(diào)試模式:
GODEBUG=schedtrace=1000,scheddetail=1 go run main.go
- schedtrace=1000 表示每隔 1000 毫秒輸出一次調(diào)度器狀態(tài)。
- scheddetail=1 表示輸出詳細(xì)的調(diào)度器信息。
#輸出內(nèi)容解釋
在輸出的調(diào)試信息中,我們可以看到調(diào)度器何時(shí)搶占了 Goroutine,何時(shí)讓出了 P,以及具體的調(diào)度行為。調(diào)試信息會(huì)包括如下內(nèi)容:
- idle M:表示某個(gè) M(線程)變成空閑狀態(tài)。
- new work:表示調(diào)度器找到了新的工作,分配給 P。
- steal work:表示調(diào)度器從其他 P 中竊取任務(wù)來(lái)運(yùn)行。
#最后我們來(lái)總結(jié)一下
- Go 的調(diào)度器主要基于 協(xié)作式調(diào)度,但是對(duì)于計(jì)算密集型任務(wù)會(huì)通過(guò) 搶占式調(diào)度 機(jī)制防止長(zhǎng)時(shí)間占用 CPU。
- 搶占調(diào)度在計(jì)算密集型 Goroutine、系統(tǒng)調(diào)用后、垃圾回收等場(chǎng)景下被觸發(fā)。
- Go 1.14 引入了針對(duì)長(zhǎng)時(shí)間運(yùn)行的 Goroutine 的搶占式調(diào)度,使得 Goroutine 不會(huì)因?yàn)橛?jì)算密集任務(wù)長(zhǎng)時(shí)間阻塞 CPU。
這使得 Go 語(yǔ)言能更加高效地運(yùn)行并發(fā)程序,避免單個(gè) Goroutine 長(zhǎng)時(shí)間霸占 CPU,影響其他 Goroutine 的執(zhí)行。