为什么需要 GMP?
根本问题:如何用少量 OS 线程,高效运行海量并发任务?
操作系统线程很重(栈默认 1-8MB,切换需要内核介入,创建销毁开销大)。但我们想要能轻松启动 百万级并发 的语言。
这逼出了一个核心设计决策:在用户态实现调度。
三个核心实体
G(Goroutine)— 任务
- 用户态”线程”,初始栈仅 2-4KB,可动态增长
- 本质是一个描述”待执行函数”的结构体
- 状态:Runnable / Running / Waiting / Dead
M(Machine)— 执行者
- 对应一个真实的 OS 线程
- 必须绑定一个 P 才能执行 G
- 数量受
SetMaxThreads限制(默认 10000)
P(Processor)— 调度上下文
- 逻辑处理器,持有本地运行队列
- 数量 =
GOMAXPROCS(默认 = CPU 核数) - 是连接 G 和 M 的”桥梁”
1 | G G G ← 等待运行的 Goroutines |
为什么要有 P?
这是整个模型最精妙的设计。
没有 P 的朴素方案:所有 M 竞争一个全局队列 → 锁竞争严重
引入 P 的好处:
- 本地队列无锁访问 — 每个 P 有自己的队列,M 绑定 P 后直接取,不需要全局锁
- M 阻塞时 P 可转移 — M 因系统调用阻塞,P 立刻甩给其他 M,不浪费 CPU
- 全局队列作为补充 — 溢出或公平性需要时才访问
调度的核心机制
① 工作窃取(Work Stealing)
当某个 P 的本地队列为空时,不闲置,而是去”偷”别人的任务:
1 | P0 队列空闲 P1 队列繁忙 |
这保证了 CPU 始终保持忙碌,负载自动均衡。
② 系统调用处理
1 | G 发起阻塞系统调用(如文件 IO) |
③ 抢占调度
早期 Go 是协作式(只在函数调用时切换),Go 1.14 引入异步抢占:
- sysmon 监控线程每 10ms 检测运行超时的 G
- 通过信号(SIGURG)强制中断,实现真正的抢占
完整调度循环(一个 M 的视角)
1 | findRunnable(): |
数量关系总结
| 实体 | 数量 | 决定因素 |
|---|---|---|
| G | 百万级 | 用户创建,轻量 |
| P | 少量固定 | GOMAXPROCS,通常 = CPU 核数 |
| M | 动态 | 按需创建,上限 10000 |
核心约束:同时运行的 G 数量 ≤ P 数量 ≤ CPU 核数,这正是 GMP 控制并发度的本质。
一句话总结
GMP 的本质是:用 P 的数量 限制真正的并行度,用 工作窃取 保证负载均衡,用 M 与 P 的分离 处理阻塞,从而用极低的成本在用户态调度海量 Goroutine。