Go 语言 GMP 模型理解
核心三层架构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| ┌─────────────────────────────────────────────────┐
│ G (Goroutine) - 用户级线程 │
│ 轻量级、海量(百万级)、由Go调度器管理 │
└────────────┬────────────────────────────────────┘
│ 绑定关系
┌────────────▼────────────────────────────────────┐
│ M (Machine) - OS线程 / 工作线程 │
│ 真实执行单位、数量受限(默认10000)、昂贵资源 │
└────────────┬────────────────────────────────────┘
│ 绑定关系
┌────────────▼────────────────────────────────────┐
│ P (Processor) - 逻辑处理器 / 调度器 │
│ 本地队列持有者、数量=GOMAXPROCS(默认=CPU核数) │
└─────────────────────────────────────────────────┘
|
快速记忆法
| 层级 | 英文 | 中文 | 数量 | 特性 |
|---|
| G | Goroutine | 协程 | 百万+ | 用户态、廉价、可阻塞 |
| M | Machine | 线程 | ~10K | 内核态、昂贵、真执行 |
| P | Processor | 处理器 | CPU核数 | 调度器、本地队列 |
执行流程(3步)
1
2
3
4
5
| 1. G创建 → 进入P的本地队列(LRQ)
2. M从P获取G → 执行(M:P = N:1)
3. G阻塞/完成 → P寻找新G或唤醒M
|
关键机制
1. Work Stealing(工作窃取)
1
2
3
4
| P1本地队列满 P2本地队列空
↓ ↓
[G1][G2][G3] → [G1][G2]
P1将G3转移给P2(负载均衡)
|
当P2队列为空的时候,会从P1偷走一半(向上取整)
2. 自旋M(Spinning M)
- M没有G时不立即休眠,而是自旋寻找G
- 减少M唤醒延迟,提高吞吐量
- 最多GOMAXPROCS个M自旋
3. 阻塞处理
1
2
3
4
5
6
7
| G阻塞(I/O/锁)
↓
M与P解绑 → M进入阻塞等待
↓
P立即绑定新M → 继续执行队列中的G
↓
原M唤醒 → 寻找可用P或加入全局队列
|
常见问题速解
Q: 为什么需要P?
A: 避免全局队列锁竞争,P的本地队列无锁。
Q: M数量为什么有上限?
A: 每个M占用~2MB内存,防止内存爆炸。
Q: GOMAXPROCS=1时会怎样?
A: 单P单M,所有G串行执行(除非G主动让出)。
Q: 如何观察GMP状态?
1
2
| GODEBUG=gctrace=1 ./program # 查看GC与调度信息
go tool trace trace.out # 可视化调度
|
代码示例:观察GMP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// 设置P数量
runtime.GOMAXPROCS(2)
// 打印当前GMP状态
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("NumGoroutine: %d\n", runtime.NumGoroutine())
fmt.Printf("NumCPU: %d\n", runtime.NumCPU())
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(-1))
// 创建大量G
for i := range 100 {
go func(id int) {
time.Sleep(100 * time.Millisecond)
fmt.Printf("G%d done\n", id)
}(i)
}
time.Sleep(200 * time.Millisecond)
}
|
调度决策树
1
2
3
4
5
6
7
| G就绪
├─ P有空闲M? ──Yes→ 绑定执行
│
└─ No
├─ 其他P有G? ──Yes→ Work Stealing
│
└─ No → G进入全局队列(GRQ)
|
参考