目录
Please enable Javascript to view the contents

Go GMP 调度模型详解

 ·  ☕ 3 分钟

重要

GMP 是 Go 的调度器模型:G(Goroutine)是执行体,M(Machine)是 OS 线程,P(Processor)是 M 的运行上下文。调度器通过工作窃取 + 无锁本地队列实现高效并发。

1. G、M、P 的角色

组件全称角色
GGoroutine执行体,每个 goroutine 对应一个 G
MMachineOS 线程,G 必须绑定到 M 才能执行
PProcessorM 的上下文,持有本地 G 队列(LRQ)

默认 P 的数量 = GOMAXPROCS(通常等于 CPU 核心数)。每个 P 维护一个 FIFO 的本地 G 队列。

2. 工作窃取机制

当 M 上 P 的本地 G 队列空了,M 会按以下顺序寻找可执行的 G:

  1. 全局 G 队列(GRQ)取;
  2. 随机选择一个其他 P,从其本地队列尾部窃取一半 G

2.1 为什么是"随机窃取 + CAS"

窃取 = 受害者 LRQ 尾指针的修改——几次内存读取 + 整数计算 + 一次原子比较。

机制成本适用
CASCPU 空转(低)操作极短时优于锁
互斥锁线程挂起 + 内核态切换(高)长临界区

权衡公式: 操作成本 × 竞争概率 × 失败代价 越小,CAS 权重越高。

随机窃取避免了多个空闲 P 同时窃取同一个 P 的公平性问题。

3. Channel 操作中的调度协作

Channel 操作的阻塞与唤醒,展示了 GMP 三级协作模式:

1
阻塞托管 → 局部无锁移交 → 全局窃取均衡

3.1 阻塞接管

当 G 在 Channel 上阻塞时,Channel 对象直接接管 G,将其记录到内部等待队列(sendq / recvq)。执行该 G 的 M 立即释放,转去执行 P 队列中的其他 G。

关键设计:解耦调度器与同步原语。G 不属于任何调度队列(LRQ/GRQ),而是由等待的目标 Channel 直接管理。

3.2 唤醒移交(direct handoff)

当配对的 G 操作 Channel 并唤醒等待队列中的 G 时:

  1. 被唤醒的 G 直接放入当前 M-P 的 LRQ 尾部
  2. 不经过全局队列

好处:

  • 缓存亲和性:唤醒者刚操作完 Channel,相关数据极可能仍在当前 CPU 核心缓存中
  • 无锁低延迟:P 的 LRQ 写入是无锁操作

3.3 为什么优先本地入队而非并行执行

被唤醒的 G 和唤醒者串行在同一 P 上执行,牺牲了瞬时并行度。但这是局部性能优化 vs 全局负载均衡的权衡:

  • 优先保证:单次操作超低延迟 + 缓存效率
  • 补偿机制:工作窃取。如果该 P 过载,空闲 P 会从 LRQ 尾部窃取 G(新入队的 G 恰好位于尾部),重新实现并行

3.4 理想 vs 现实

Channel 算法的假设与现实的出入:

假设现实
缓存亲和性一定带来收益若 LRQ 被窃取,虽然得到并发但丢失缓存;若 LRQ 中穿插无关 G,也会冲刷缓存

为什么仍保持这个设计 :调度器优化是概率性的——不追求每次完美,而是通过提高有利事件的统计概率来提升整体性能期望。

4. 总结

  1. GMP 是三级模型:G 执行体、M 线程、P 上下文;
  2. 工作窃取用 CAS 替代锁——用极低的 CPU 空转替代极高的线程挂起开销;
  3. Channel 阻塞与唤醒是阻塞托管 → 局部移交 → 全局窃取的三级协作;
  4. Go 调度器的设计哲学:先吃局部确定红利,再靠全局均衡消化副作用。

5. 参考

分享

Hex
作者
Hex
CloudNative Developer