GOMAXPROCS 设置的是 P 的数量,决定并发执行上限;P 是 G 被 M 执行的必要上下文,无 P 则 M 无法工作,因此该值直接影响调度效率、CPU 利用率及 Goroutine 是否饥饿或卡顿。

Go 的并发不是靠操作系统线程硬扛出来的,而是靠 G、P、M 三者协作完成的——这个模型不抽象,它直接决定你写的 go 语句会不会卡住、会不会饿死、会不会浪费 CPU。
为什么不能只靠 goroutine(G)?
很多人以为 go 一开就并发,其实不是。每个 G 必须被调度到一个 M 上才能跑,而 M 又必须绑定一个 P 才能干活。没有 P,M 就是“没工牌的工人”,进不了车间。所以 GOMAXPROCS 设置的是 P 的数量,它才是并发执行的上限,不是 G 数量,也不是 M 数量。
常见错误现象:
- 写了一堆
go func() {...}(),但 CPU 利用率始终上不去 → 很可能是GOMAXPROCS被设成了 1 - 大量
G阻塞在 channel 或锁上,但其他G却迟迟得不到调度 →P本地队列空了,没触发工作窃取(work stealing)
系统调用阻塞时,M 和 P 怎么不“卡死”?
当 G 进入系统调用(比如 read、write、accept),它会把绑定的 M 一起拖进阻塞态。这时 Go 运行时会立刻解绑 M 和 P,让 P 去找另一个空闲 M 继续跑别的 G。被阻塞的 M 完成系统调用后,会尝试抢回原 P;抢不到就去全局队列或别的 P 偷任务。
立即学习“go语言免费学习笔记(深入)”;
关键点:
-
M阻塞 ≠P闲置 → 这是 GPM 区别于老式 GM 模型的核心改进 - 网络 I/O 默认走
netpoller(epoll/kqueue),不阻塞M,所以高并发 HTTP 服务才不会因连接数多而线程爆炸 - 如果用了
syscall.Syscall这类阻塞式系统调用,又没配runtime.LockOSThread,就可能触发 M 大量创建
本地队列(LRQ)和全局队列(GRQ)怎么影响性能?
P 的本地队列最多存 256 个 G,新 G 默认往这儿塞;满了才丢进全局队列 GRQ。但 GRQ 是全局锁保护的,争抢激烈;而 LRQ 是无锁的,取 G 极快。
所以实际影响很明显:
- 短平快任务(如简单计算、小 channel 通信)尽量让它们落在同一个
P的 LRQ 里,减少跨 P 调度开销 - 如果某个
P一直处理慢任务(比如大循环、长 sleep),它的 LRQ 空了,就会去偷别的P的一半G—— 这叫 work stealing,但偷的过程有开销 - 频繁创建
G(比如每请求都go handle())且不控制总量,容易导致 LRQ 溢出 + GRQ 拥塞 + 锁竞争加剧
真正难调试的,从来不是“为什么并发不起来”,而是“为什么明明有空闲 CPU,某些 G 就是等了 10ms 才轮到”。这时候得看 P 是否被长期独占、M 是否在系统调用里滞留太久、或者 GRQ 是否成了瓶颈——这些细节不在代码里,但在运行时行为里。


















