1012 字
5 分钟
GMP调度模型
2025-12-10

一、GMP 到底是什么?#

GMP 是三个核心组件:

  • G(Goroutine):用户级协程
  • M(Machine):操作系统线程
  • P(Processor):调度器的上下文(可以理解为“执行配额 + 本地队列”)

👉 一句话总结: G 是任务,M 是执行者,P 是调度权 + 资源容器

二、为什么需要 P#

如果只有 G 和 M,会怎样?

早期模型(类似线程池):

  • G → 直接分配给 M
  • 会出现严重问题:
    • 线程频繁切换(系统调用成本高)
    • 锁竞争严重(全局队列)
    • cache 不友好

👉 Go 引入 P,本质是为了:

✅ 1. 降低锁竞争#

  • 每个 P 有 本地 G 队列
  • 绝大多数调度在本地完成,不用抢全局锁

✅ 2. 控制并行度#

  • P 的数量 = GOMAXPROCS
  • 限制同时运行的 goroutine 数量

✅ 3. 绑定执行上下文#

P 持有:

  • 本地运行队列
  • 内存缓存(mcache)
  • 调度状态

👉 可以理解为:

P = “CPU 核 + 本地任务队列 + 调度权限”

三、调度过程(核心流程)#

1️⃣ 创建 goroutine#

go func() { … }

发生什么?

  • 创建一个 G
  • 放入:
    • 当前 P 的本地队列(优先)
    • 或全局队列(极少)

2️⃣ 执行流程#

P -> 取 G -> 交给 M 执行

更完整:

M 必须绑定 P 才能运行 G

👉 关系是:

G ---被调度---> P ---绑定---> M


3️⃣ 本地队列 vs 全局队列#

每个 P:

  • 本地队列(最多 ~256 个 G)
  • 调度优先级:

本地队列 > 全局队列


4️⃣ Work Stealing(偷任务机制)#

当一个 P 空了:

👉 它会去别的 P “偷一半任务”

P1: [G1 G2 G3 G4]
P2: []

P2 偷 → [G3 G4]

👉 好处:

  • 负载均衡
  • 减少全局锁依赖

四、阻塞时发生什么(重点)#

场景:G 发生系统调用(如 IO)#

read(fd)

❗问题:#

  • M 被阻塞
  • 那这个 P 怎么办?

Go 的处理方式:#

🔁 M 阻塞时:#

  1. M 进入阻塞状态
  2. P 被剥离
  3. P 转移给新的 M
  4. 继续执行其他 G

👉 效果:

阻塞的是线程(M),不是调度能力(P)


类比一下:#

  • M = 工人
  • P = 工位
  • G = 任务

👉 如果工人被卡住:

  • 工位不会空着
  • 换一个工人继续干

五、syscall vs goroutine 阻塞#

🟢 goroutine 阻塞(channel / mutex)#

<-ch

👉 不会阻塞 M:

  • G 被挂起
  • M 继续执行其他 G

🔴 syscall 阻塞(IO)#

syscall.Read()

👉 会阻塞 M:

  • runtime 把 P 抢走
  • 让新 M 接手

六、调度触发点(什么时候切换 G)#

Go 不是完全抢占式(早期),现在是“半抢占式”:

常见切换点:#

  • channel 操作
  • syscall
  • 函数调用(编译器插入检查)
  • GC safepoint
  • 时间片用尽(Go 1.14+ 支持抢占)

七、GMP 关系图(文字版)#

+--------+
| G | goroutine(任务)
+--------+
|
v
+--------+
| P | 本地队列 + 调度权
+--------+
|
v
+--------+
| M | OS线程
+--------+
|
v
CPU

八、为什么 GMP 很强?#

总结它的设计优势:

✅ 1. 高并发#

  • goroutine 很轻(KB 级栈)

✅ 2. 高效调度#

  • 本地队列 + work stealing

✅ 3. 减少锁竞争#

  • 避免全局队列争抢

✅ 4. IO 不阻塞整体#

  • P 可转移

✅ 5. 自动扩缩#

  • M 数量动态变化

九、一句话理解 GMP#

用少量线程(M)高效执行海量协程(G),通过调度器(P)实现低锁、高并发、可扩展的执行模型

NOTE

Go 的 GMP 调度模型是它实现高并发的核心机制。G 是 goroutine,表示任务;M 是操作系统线程,负责执行;P 是调度器的上下文,维护本地队列并控制并行度。调度时,每个 P 会优先从自己的本地队列取 G 交给 M 执行,如果本地队列为空,会从全局队列或其他 P 通过 work stealing 偷任务来执行。当 goroutine 发生阻塞时,比如 channel 阻塞,不会阻塞线程;但如果是系统调用导致线程阻塞,runtime 会把 P 从当前 M 上解绑,交给新的 M 继续执行,从而保证整体吞吐。通过这种设计,Go 在减少锁竞争的同时,实现了高效的并发调度。

GMP调度模型
https://blog.sleepwf.dev/posts/gmp调度模型/
作者
Sleepwf
发布于
2025-12-10
许可协议
CC BY-NC-SA 4.0