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