Go資深工程師講解(慕課) 008_GMP調度器與Go設計哲學

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 的本地隊列爲空時:

  1. 先從全局隊列取 G
  2. 全局隊列也沒有 → 隨機從其他 P 的隊列偷一半 G
  3. 都沒有 → 檢查網絡輪詢器
  4. 還沒有 → 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 異步處理:

  1. G 發起網絡調用 → G 被掛到 netpoller
  2. M 不阻塞,繼續運行其他 G
  3. 網絡就緒時 → 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

(0)
Walker的頭像Walker
上一篇 13小時前
下一篇 21小時前

相關推薦

  • Go工程師體系課 015

    Docker 容器化 —— Go 項目實戰指南 一、Docker 核心概念 1.1 什麼是 Docker Docker 是一個開源的容器化平臺,它可以將應用程序及其所有依賴項打包到一個標準化的單元(容器)中,從而實現"一次構建,到處運行"。對於 Go 開發者而言,Docker 解決了以下痛點: 開發環境與生產環境不一致 依賴管理複雜(數據庫、緩存、消息隊列等…

  • Go工程師體系課 008

    訂單及購物車 先從庫存服務中將 srv 的服務代碼框架複製過來,查找替換對應的名稱(order_srv) 加密技術基礎 對稱加密(Symmetric Encryption) 原理: 使用同一個密鑰進行加密和解密 就像一把鑰匙,既能鎖門也能開門 加密速度快,適合大量數據傳輸 使用場景: 本地文件加密 數據庫內容加密 大量數據傳輸時的內容加密 內部系統間的快速通…

    後端開發 8小時前
    100
  • 編程基礎 0008_標準庫進階

    Go 標準庫進階 系統整理 Go 標準庫中最常用的包,重點覆蓋 io、os、bufio、strings、time、fmt 等 1. io 包核心接口 Go 的 I/O 設計圍繞幾個核心接口展開,幾乎所有 I/O 操作都基於它們。 // 最基礎的兩個接口 type Reader interface { Read(p []byte) (n int, err er…

    後端開發 19小時前
    500
  • Go工程師體系課 019

    Go 內存模型與 GC 1. 內存分配基礎 1.1 棧(Stack)與堆(Heap) ┌─────────────────────────────┐ │ 堆 (Heap) │ ← 動態分配,GC 管理 │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │ obj │ │ obj │ │ obj │ │ │ └─────┘ └─────┘ └────…

  • Go資深工程師講解(慕課) 007_godoc與代碼生成

    Go 文檔生成與示例代碼 對應視頻 8-6 生成文檔和示例代碼 1. godoc 文檔生成 Go 的文檔直接從源碼註釋中提取,不需要特殊標記語法。 1.1 註釋規範 // Package queue 實現了一個簡單的 FIFO 隊列。 // // 該隊列基於切片實現,支持 Push、Pop 和 IsEmpty 操作。 package queue // Que…

簡體中文 繁體中文 English