p本地队列满(256个)时将goroutine放入无长度限制但需加锁的全局队列,是为避免队列膨胀、保障窃取效率与内存局部性;窃取时取目标队列长度一半(向下取整),从尾端取以避免竞争。

为什么 P 的本地队列满了就往全局队列扔 goroutine?
因为每个 P 的本地运行队列(LRQ)有硬性容量限制:256 个 G。一旦超过,新创建的 goroutine 就不会塞进本地队列,而是直接入全局队列(GRQ)。这不是 bug,是设计——避免单个 P 队列无限膨胀,影响窃取效率和内存局部性。
- 本地队列满时,
newproc会调用globrunqput把 goroutine 放进全局队列 - 全局队列无长度限制,但访问需加锁,所以只作为“后备缓冲”,不是主路径
- 如果你看到 trace 中
runqueue显示类似[256 0] 12(两个本地队列 + 全局队列长度),说明已有 12 个 goroutine 被“挤出”到全局队列了
work stealing 窃取时到底拿走几个 goroutine?
调度器不会全拿走,而是拿一半——准确说是 len(target.runq)/2 向下取整。比如目标 P 本地队列有 7 个 G,当前 P 窃取时会拿走 3 个(不是 4 个,Go 实现用的是右移:7>>1 == 3)。
- 这个“一半”策略平衡了负载迁移开销和均衡效果:拿太少起不到作用,拿太多会导致原
P下次立即空转 - 窃取操作从目标队列的**尾端(tail)** 取,而原
P自己执行是从**头端(head)** 取,天然避免读写竞争 - 如果目标队列只有 1 个
G,窃取数量为 0;只有 ≥2 才可能被偷到
为什么有时明明有 goroutine 却卡住不调度?
常见于低并发、短生命周期场景,本质是调度器“懒”:它只在真正需要时才启动窃取流程,且每轮最多尝试 4 次随机窃取,失败即放弃进入休眠。
- 典型现象:
runtime.gosched或系统调用后,goroutine 长时间没被唤醒,pprof显示 M 处于_M_RUNNABLE但实际没执行 - 根本原因:当前
P本地队列空 → 全局队列空 → netpoller 无就绪 G → 四轮窃取全部失败 → 进入park_m睡眠 - 解决思路不是“强制唤醒”,而是确保有足够活跃 goroutine 分布在多个
P上;可通过GOMAXPROCS=2强制多 P,或让 goroutine 主动 yield(如插入runtime.Gosched())触发重调度
如何验证当前程序是否发生了 work stealing?
不能靠日志或 debug,得用 Go 内置 trace 工具抓底层调度事件,重点关注 steal 和 runnable 类型事件。
立即学习“go语言免费学习笔记(深入)”;
- 运行命令:
go run -gcflags="-l" -trace=trace.out main.go && go tool trace trace.out - 在 Web UI 中打开后,点顶部 “View trace” → 拉到底部看 “Proc” 行,若某
P在空闲后突然出现绿色 “Running” 块,且左侧标注 “steal” 字样,就是窃取成功 - 注意:trace 默认不记录窃取细节,要看到具体从哪个 P 偷了多少,需配合
GOROOT/src/runtime/trace.go中的traceGoSched和traceGoSteal手动 patch(生产环境慎用)
调度器的“窃取”不是实时的,也不是贪婪的——它只在确认自己真没活干时,才伸手去别人家翻一半篮子。很多人误以为只要开了多核就自动均摊,其实 goroutine 创建位置、阻塞行为、甚至 for 循环里有没有函数调用,都会影响它们最终落在哪个 P 上。这点在写高吞吐网络服务时,比加机器还关键。










