work stealing 是 Go 调度器在 findrunnable 函数中、当本地队列和全局队列均空且 netpoll 无就绪 G 时必须触发的负载再分配机制,非后台线程或定时扫描,而是“饿了才动”的协作式窃取。
work stealing 不是 go 调度器的“可选优化”,而是 m 在找不到活干时必须触发的负载再分配机制——它只在当前 p 的本地队列(runq)为空、全局队列(globrunq)也空、netpoll 无就绪 g 时才启动,不是后台线程,也不是定时扫描。
work stealing 触发时机:findrunnable 函数里“饿了才动”
调度器不会预判或主动均衡。它只在 findrunnable 函数中按固定顺序查三处:_p_.runq → globrunq → netpoll。全部失败后,才进入窃取流程。
- 哪怕其他 P 队列有 200 个 G,只要当前 P 还没查完这三步,就不会偷
- 一旦某次成功偷到 1 个 G,立刻返回执行,不等凑够一批
- 若四轮随机尝试(
stealTries = 4)全失败,M 就进入 park 状态,等被唤醒
从哪偷、偷多少:伪随机 + 尾部取一半
偷的目标不是“最忙的 P”,而是用互质偏移遍历所有 P,保证每轮都有机会访问每个 P;偷的数量不是固定值,而是目标 P 当前 runq 长度的一半(向下取整),且上限 256。
- 目标队列有 7 个 G → 偷
7 >> 1 == 3个;只有 1 个 → 偷 0 个(本轮失败) - 偷操作从目标 P 的
runqtail端取,而该 P 自己消费走runqhead,天然避免竞争 - 源码中核心逻辑是
n := (t - h) / 2,其中t和h是原子读取的尾/头指针
为什么不用锁也能安全偷:双端队列 + 原子方向隔离
每个 P 的本地队列是 lock-free 双端结构,安全不靠锁,靠分工:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 本 P 只改
runqhead(出队),其他 P 只改runqtail(入队)和尝试性更新runqhead - 偷失败时,
runqgrab内部用 CAS 更新runqhead,失败者直接跳过,不重试也不阻塞 - 全局队列
globrunq才需要globalRunqLock,所以本地队列优先级永远更高
常见误判和实际影响:别指望 runtime 替你平衡用户任务
标准库的 work stealing 对你手写的 goroutine 池、分片任务、自定义 worker 队列完全不感知。它只搬 G,不理解你的业务逻辑。
立即学习“go语言免费学习笔记(深入)”;
- pprof 显示某些 P 的 goroutine 数长期远高于其他 P?那大概率是你没做用户层 task 分散,runtime 不会帮你拆函数
- 大量 goroutine 阻塞在系统调用(如
read)→ P 被抢占释放 → 新 G 全挤进globrunq→work stealing不查globrunq,只查本地队列 → 实际等于没偷 -
GOMAXPROCS调大只增加 P 数量,不改变窃取阈值或策略;真正可控的调节面只有两个:GOMAXPROCS和 GC 频率(GC STW 会干扰 steal 信号)
真正容易被忽略的点在于:你看到的“调度均衡”往往是假象——当所有 P 的本地队列都塞满长耗时任务时,新 goroutine 只能排队等,根本不会被偷。这时候,work stealing 依然在运行,但它已经无 G 可偷。

















