work stealing 在 findrunnable 函数中触发,当 m 绑定的 p 本地队列为空时立即执行:先查全局队列,再查 netpoll,最后随机窃取其他 p 队尾一半 g(向下取整),最多尝试 4 轮。

work-stealing 从哪开始触发?
当一个 M 绑定的 P 本地队列为空时,它不会立刻休眠或阻塞,而是启动 work-stealing 流程。这个判断发生在 findrunnable() 函数里——不是等 M 空转几毫秒才查,而是每次取不到 G 就立即行动。
触发顺序固定:
- 先尝试从全局队列 runtime.runq 拿一批(默认最多 32 个)
- 再检查网络轮询器 netpoller 是否有就绪的 G(比如 channel receive、TCP accept 完成)
- 最后才去其他 P 的本地队列偷:随机选一个 P,从其队尾取一半 G(避免影响原 P 队头任务的局部性)
为什么是“偷一半”而不是全偷或只偷一个?
偷一半是平衡吞吐与公平的关键设计。偷太多,原 P 下次可能没活干;偷太少,新 P 很快又空转,浪费 CPU。
实际逻辑在 stealWork() 中:
- 检查目标 P 的本地队列长度 len(p.runq)
- 若 ≥2,则取 len/2 向下取整(如队列有 7 个,偷 3 个)
- 若只有 1 个,不偷(避免频繁抖动)
- 偷完后,被偷的 P 仍保有至少一半,能继续服务后续新创建的 G
work-stealing 会破坏缓存局部性吗?
会,但可控。G 在原 P 创建时通常共享栈、内存分配上下文,迁移到另一个 M 执行后,CPU cache line 可能失效。不过 Go 运行时做了两层缓解:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 偷取时从队尾拿,保留原 P 队头最近活跃的 G,减少热数据迁移
- 新偷来的 G 优先在当前 M 上执行,不会立刻再被偷走(没有“连环偷”)
- 实测中,只要 P 数量 ≤ CPU 物理核数,cache miss 率上升通常
哪些场景会让 work-stealing 失效或变慢?
work-stealing 不是万能负载均衡器,以下情况它基本不起作用:
- 所有 P 的本地队列都非空:根本不会触发偷取逻辑
- 全局队列和 netpoller 都为空,且所有其他 P 队列长度 ≤1:偷取条件不满足,M 进入自旋或休眠
- 程序大量使用 runtime.LockOSThread():绑定 M 到特定线程后,该 M 无法自由切换 P,偷取路径被绕过
- GOMAXPROCS=1:只有一个 P,无其他 P 可偷,退化为纯本地队列 + 全局队列模式
真正要注意的是:work-stealing 效果高度依赖 P 的数量和 G 的分布均匀性。如果业务逻辑导致某些 P 长期积压(比如一个 P 负责处理所有数据库连接),偷取只能缓解,不能根治——这时得从代码层面拆分热点 Goroutine,而不是调大 GOMAXPROCS。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










