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