Go GMP 調度器與設計哲學
對應視頻 9-2 go語言的調度器、18-1 體會Go語言的設計、18-2 課程總結
1. Go 調度器演進
1.0 時代:單線程調度器(Go 0.x)
- 只有一個線程運行 goroutine
- 所有 goroutine 排隊等待
- 無法利用多核
1.1 時代:多線程調度器(Go 1.0)
- 引入多線程
- 但全局鎖競爭嚴重,性能瓶頸
1.2+ 時代:GMP 模型(Go 1.2 至今)
引入了著名的 GMP 調度模型。
2. GMP 模型詳解
G (Goroutine) - 協程,用戶級輕量線程
M (Machine/Thread) - 操作系統線程
P (Processor) - 邏輯處理器,調度上下文
2.1 三者的關係
┌─────────┐
│ Go 程序 │
└────┬────┘
│ 創建 goroutine
┌────────────┼────────────┐
▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────┐
│ G │ │ G │ │ G │ ... (成千上萬個)
└──┬──┘ └──┬──┘ └──┬──┘
│ │ │
┌─────┴─────┐ │ ┌────┴─────┐
│ P 的本地隊列│ │ │ P 的本地隊列│
│ [G][G][G] │ │ │ [G][G] │
└─────┬─────┘ │ └────┬─────┘
│ │ │
┌──┴──┐ ┌──┴──┐ ┌──┴──┐
│ P │ │ P │ │ P │ (GOMAXPROCS 個)
└──┬──┘ └──┬──┘ └──┬──┘
│ │ │
┌──┴──┐ ┌──┴──┐ ┌──┴──┐
│ M │ │ M │ │ M │ (按需創建)
└──┬──┘ └──┬──┘ └──┬──┘
│ │ │
══════╧═══════════╧══════════╧══════
操作系統內核線程
2.2 各組件詳解
G (Goroutine)
- 初始棧大小僅 2KB(線程通常 1-8MB)
- 棧可動態增長和收縮
- 包含執行的函數、棧指針、程序計數器等
- 狀態:可運行、運行中、等待中、已完成
M (Machine)
- 對應一個操作系統線程
- 由 Go runtime 按需創建,默認最多 10000 個
- M 必須持有 P 才能運行 G
- 當 M 因系統調用阻塞時,P 會被搶走給其他 M
P (Processor)
- 數量由 GOMAXPROCS 決定(默認等於 CPU 核數)
- 每個 P 有一個本地運行隊列(最多 256 個 G)
- P 是調度的核心,決定哪個 G 在哪個 M 上運行
2.3 調度策略
// 查看和設置 P 的數量
runtime.GOMAXPROCS(0) // 獲取當前值
runtime.GOMAXPROCS(4) // 設置為 4
runtime.NumCPU() // CPU 核數
調度時機(goroutine 可能的切換點):
| 切換點 | 說明 |
|---|---|
| I/O 操作 | 文件、網絡讀寫 |
| channel 操作 | 發送/接收阻塞時 |
| select | 多路復用 |
| 等待鎖 | sync.Mutex 等 |
| 函數調用 | 編譯器在函數入口插入檢查點 |
runtime.Gosched() |
手動讓出 |
| GC | 垃圾回收的 STW 階段 |
| 系統調用 | syscall 阻塞時 M 和 P 分離 |
2.4 Work Stealing(工作竊取)
當一個 P 的本地隊列為空時:
- 先從全局隊列取 G
- 全局隊列也沒有 → 隨機從其他 P 的隊列偷一半 G
- 都沒有 → 檢查網絡輪詢器
- 還沒有 → M 休眠
P1 [空] P2 [G5 G6 G7 G8]
│ │
│ 偷一半! │
│ ◄───── steal ───── │
│ │
P1 [G7 G8] P2 [G5 G6]
2.5 系統調用處理(Hand Off)
當 G 執行系統調用(如文件 I/O)阻塞 M 時:
正常狀態: M1 ──── P1 ──── G1(syscall阻塞)
Hand Off: M1 ──── G1(繼續阻塞在syscall)
M2 ──── P1 ──── G2(P被轉給新的M)
P 會被轉給空閒的 M(或新建一個 M),不讓 P 閒著。
2.6 網絡輪詢器(netpoller)
網絡 I/O 不會阻塞 M,而是使用 epoll/kqueue 異步處理:
- G 發起網絡調用 → G 被掛到 netpoller
- M 不阻塞,繼續運行其他 G
- 網絡就緒時 → G 被放回可運行隊列
這就是為甚麼 Go 能高效處理大量網絡連接。
3. Goroutine 與線程的對比
| 特性 | Goroutine | OS Thread |
|---|---|---|
| 棧大小 | 2KB(動態增長) | 1-8MB(固定) |
| 創建成本 | ~0.3μs | ~30μs |
| 切換成本 | ~0.2μs(用戶態) | ~1μs(內核態) |
| 調度方式 | 協作式+搶佔式 | 搶佔式 |
| 數量級 | 百萬級 | 千級 |
| 通信方式 | channel | 共享內存+鎖 |
搶佔式調度(Go 1.14+)
Go 1.14 之前是純協作式調度,密集計算的 goroutine 可能長時間佔用 M:
// Go 1.14 之前,這個會獨佔線程
go func() {
for {} // 死循環,不讓出
}()
Go 1.14+ 引入了基於信號的搶佔(SIGURG),即使沒有函數調用也能被搶佔。
4. 實際觀察調度器
# GODEBUG 查看調度器狀態
GODEBUG=schedtrace=1000 go run main.go
# 每 1000ms 輸出調度器狀態
# 更詳細
GODEBUG=schedtrace=1000,scheddetail=1 go run main.go
# go tool trace 可視化
// 在代碼中查看
fmt.Println("goroutine數:", runtime.NumGoroutine())
fmt.Println("CPU核數:", runtime.NumCPU())
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
5. Go 語言的設計哲學
對應視頻 Ch18:體會Go語言的設計
5.1 少即是多(Less is more)
- 只有 25 個關鍵字(C 有 32 個,Java 有 50+)
- 沒有 class,用 struct + method 替代
- 沒有繼承,用組合(embedding)替代
- 沒有泛型(Go 1.18 前)—— 保持簡單
- 沒有異常,用多返回值 + error 替代 try/catch
- 只有 for 循環,沒有 while/do-while
- 沒有三元運算符
?:
5.2 組合優於繼承
// 不是繼承,是組合
type Animal struct {
Name string
}
func (a Animal) Speak() string { return a.Name + " speaks" }
type Dog struct {
Animal // 嵌入,不是繼承
Breed string
}
// Dog 自動獲得 Speak 方法,但可以覆蓋
5.3 接口的隱式實現
// 不需要 implements 關鍵字
type Stringer interface {
String() string
}
// 任何有 String() string 方法的類型自動實現了 Stringer
// → 解耦了定義者和實現者
5.4 併發是一等公民
go關鍵字啓動協程(不是庫函數)chan是內置類型(不是庫中的 Queue)select是語言級別的多路復用- "Don't communicate by sharing memory; share memory by communicating."
5.5 工具鏈哲學
| 工具 | 作用 |
|---|---|
gofmt |
統一代碼風格,沒有風格戰爭 |
go vet |
靜態分析,找常見錯誤 |
go test |
內置測試框架,不需要第三方 |
go doc |
注釋即文檔 |
go build |
編譯成單一二進制,無依賴 |
go mod |
內置依賴管理 |
5.6 錯誤處理哲學
// 顯式處理每個錯誤,不會被隱藏
f, err := os.Open("file.txt")
if err != nil {
// 必須處理
}
// 雖然"囉嗦",但:
// 1. 錯誤路徑清晰可見
// 2. 不會因為忘記 catch 而崩潰
// 3. 強迫你思考每個可能的失敗
5.7 Go 箴言(Go Proverbs by Rob Pike)
| 箴言 | 含義 |
|---|---|
| Don't communicate by sharing memory, share memory by communicating | 用 channel 而非鎖 |
| Concurrency is not parallelism | 併發是結構,並行是執行 |
| Channels orchestrate; mutexes serialize | channel 編排流程,mutex 串行化訪問 |
| The bigger the interface, the weaker the abstraction | 接口越小越好 |
| Make the zero value useful | 零值應該可直接使用 |
| interface{} says nothing | 空接口不表達任何信息 |
| Gofmt's style is no one's favorite, yet gofmt is everyone's favorite | 統一風格比好看重要 |
| A little copying is better than a little dependency | 少量複製好過引入依賴 |
| Clear is better than clever | 清晰勝過聰明 |
| Errors are values | 錯誤是值,可以編程處理 |
| Don't just check errors, handle them gracefully | 優雅處理錯誤,不只是檢查 |
| Don't panic | 不要輕易 panic |
6. 課程總結思維導圖
Go 語言核心
├── 基礎語法
│ ├── 變量、常量、類型
│ ├── 控制流(if/for/switch)
│ └── 函數(多返回值、閉包、defer)
├── 面向對象
│ ├── struct + method(代替 class)
│ ├── 組合(代替繼承)
│ └── interface(隱式實現、duck typing)
├── 函數式編程
│ ├── 閉包、高階函數
│ ├── 裝飾器/中間件模式
│ └── Functional Options
├── 錯誤處理
│ ├── error 接口、多返回值
│ ├── panic/recover(僅用於不可恢復錯誤)
│ └── 統一錯誤處理(errWrapper 模式)
├── 測試
│ ├── 表格驅動測試
│ ├── 性能測試(Benchmark)
│ ├── pprof 性能分析
│ └── Example 文檔測試
├── 併發編程
│ ├── goroutine(GMP 調度模型)
│ ├── channel(CSP 通信模型)
│ ├── select(多路復用)
│ └── sync 包(WaitGroup、Mutex)
├── 標準庫
│ ├── net/http
│ ├── encoding/json
│ └── html/template
└── 工程實踐
├── 單任務→併發→分布式爬蟲
├── ElasticSearch 集成
├── Docker 容器化
└── RPC 分布式通信
主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/6752