go 1.1 前的 gm 模型因全局队列锁竞争、无局部性、无法控制并行度而被替换;gmp 引入 p 作为调度上下文,实现无锁本地队列、work stealing 负载均衡及 m/p 解耦,支撑高并发。

Go 1.1 之前用的是 GM 模型,有严重锁竞争;1.1 引入 P 后变成 GMP,本地队列 + work stealing 才真正撑起高并发。
为什么旧版 GM 模型必须被替换
Go 1.0 的调度器只有 G 和 M,所有 G 都挤在同一个全局队列里。每次 M 想取一个 G 执行,都得抢同一把锁 —— 高并发下锁争抢直接拖垮性能。
-
runtime.runqget和runtime.runqput频繁加解锁,成为瓶颈 - 没有局部性:
M取G时 cache 命中率低,CPU 浪费明显 - 无法控制并行度:
GOMAXPROCS无处落脚,调度完全依赖 OS 线程数
GMP 中 P 的真实作用不是“处理器”,而是调度上下文
P 不是 CPU 核心,也不是线程,它是一块内存结构,封装了:runq(本地队列)、runqhead/runqtail、status、GC 相关字段等。它的核心价值是解耦 G 和 M,让调度可预测、可隔离。
-
P数量由GOMAXPROCS控制,默认等于逻辑 CPU 数,但可动态调整 - 每个
P维护最多 256 个G的本地队列,无锁操作,快且安全 -
M必须绑定P才能执行G;M阻塞时会释放P,其他空闲M可立即接管
work stealing 不是“偷”,而是负载再平衡的关键机制
当某个 P 的本地队列空了,它不会立刻休眠,而是主动去别的 P 那里“拿一半” —— 这个动作叫 work stealing,目的是防止 CPU 闲置,不是为了抢资源。
- 优先从全局队列
runtime.runq拿,避免饥饿 - 全局队列为空时,才随机选一个其他
P,取其runq中后半段(runqtail - runqhead的一半) - 偷完立刻重试调度循环,不等待、不通知,纯异步
- 注意:
runtime.globrunqget有锁,而runtime.runqget是无锁的,这就是性能差异来源
系统调用阻塞时 M 与 P 的分离容易被误解
很多人以为 M 阻塞 = 整个 P 卡死,其实不然:M 进入系统调用前会主动解绑 P,然后 P 立即被其他空闲 M 接管继续跑 G。这才是 Go 能做“大量并发 IO”的底层保障。
- 网络 IO 走
netpoller(epoll/kqueue),不阻塞M,P始终在线 - 文件 IO 或其他阻塞调用,
M会进入_Msyscall状态,P被移交 - 阻塞结束后,
M尝试重新获取P;若失败,则把完成的G放回全局队列,由其他M拉走 - 极端情况(如
M长时间阻塞),sysmon会检测并触发抢占或回收
真正难调试的点不在模型本身,而在 P 的状态迁移边界 —— 比如 _Psyscall 到 _Prunning 的时机、runtime.acquirep 失败后的 fallback 行为,这些细节不看源码几乎无法准确预判。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











