GMP能让10万个goroutine不卡死,因其将调度移至用户态,G切换无需系统调用,开销降至纳秒级;M可轮转执行大量G,且G、M、P动态解耦——G阻塞时M立即解绑P,由其他M接手,全程不等待系统调用返回。
为什么 GMP 能让 10 万个 goroutine 不卡死
因为 gmp 把调度从内核态搬到了用户态,g 切换不用陷进系统调用,开销从微秒级降到纳秒级。一个 m(os 线程)可以轮转执行成百上千个 g,而传统线程模型里,每个线程都得单独调度、分配栈、维护上下文,10 万线程直接压垮内核调度器。
关键不在“数量多”,而在“解耦”:G 不绑定 M,M 不绑定 P,P 的数量(GOMAXPROCS)可调,三者动态配对。比如一个 G 阻塞在 read() 上,M 会立刻解绑 P,另一个空闲 M 接手这个 P 继续跑其他 G——整个过程不等系统调用返回,也不停机。
findrunnable() 怎么决定下一个该跑哪个 G
调度核心函数 findrunnable() 按固定优先级查四类来源,顺序不能错:
- 当前 P 的本地队列(无锁,最快,优先取头部)
- 全局队列(加锁,一次批量取最多 128 个,减少锁争用)
- 其他 P 的本地队列尾部(work stealing,偷一半,避免和对方抢头部)
- netpoller 就绪的 G(网络 I/O 完成后唤醒的,高优先级)
注意:本地队列容量上限是 256,满了就往全局队列扔;偷窃时只从尾部拿,是为了和目标 P 自己从头部取互不干扰——这点容易被忽略,但直接影响缓存命中率和锁竞争。
GOMAXPROCS 设太高反而拖慢 CPU 密集型任务
GOMAXPROCS 控制的是 P 的数量,不是线程数,也不是 goroutine 数。它实际影响的是并行度上限:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- CPU 密集型场景(如矩阵计算、JSON 解析),设成大于 CPU 核心数,会导致频繁的 P 切换和 M 抢占,上下文切换开销反超收益
- I/O 密集型场景(如 HTTP server),适当调高(比如 2×CPU 核心数)能缓解阻塞系统调用导致的 P 饥饿
- 默认值是
runtime.NumCPU(),改之前先用pprof看runtime/pprof/sched中的SchedYield和SchedPreempt是否异常高
很多线上服务盲目设 GOMAXPROCS=128,结果 CPU cache miss 暴涨,吞吐不升反降——P 太多,G 在不同 P 间跳来跳去,局部性全毁。
阻塞系统调用时 M 和 P 是怎么解绑又重连的
这是 GMP 区别于用户态协程(如 libco)的关键机制。当 G 执行 open()、read() 等阻塞系统调用时:
- M 进入内核态等待,但运行时会立刻把 P 交给其他空闲 M(如果有)继续调度
- 若没有空闲 M,运行时会创建新 M;原 M 返回后,若 P 已被占用,就把 G 放入全局队列,自己休眠
- netpoller 机制接管网络 I/O:G 等待 socket 可读时,不真正阻塞 M,而是注册事件后让 M 去跑别的 G
真正的坑在于 Cgo 调用:它会强制把 M 锁定在某个 P 上,且无法解绑。一旦 C 函数阻塞,整个 P 就卡住,其他 G 全得等——这不是 bug,是设计约束,必须用 runtime.LockOSThread() 显式管理,或改用非阻塞 C 接口。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










