全局队列不常被用到,因其是加锁的全局共享结构,访问成本高;调度器仅以1/61概率检查它以防饿死,优先使用无锁、快速、缓存友好的P本地队列。

全局队列(sched.runq)不是“主调度池”,而是防饿死的备用通道,它不参与负载均衡主动分发,只在特定条件下被低概率检查或被动填充。
为什么全局队列不常被用到?
调度器默认优先走 P 的本地队列(pp.runq),因为无锁、快、缓存友好;全局队列是全局共享的,访问需加锁(sched.lock),竞争成本高。所以 runtime 故意降低其使用频率——只有当 pp.schedtick % 61 == 0 时才尝试从 sched.runq 取一个 G,这个 1/61 概率是权衡后的结果:太频繁会拖慢所有 P,太稀疏会导致新 goroutine 在全局队列里“躺平”太久。
常见错误现象:go f() 创建大量 goroutine 后,部分迟迟不执行,尤其在单核或 GOMAXPROCS=1 场景下更明显——本质是它们全堆在 sched.runq 里,而唯一那个 P 很少轮询它。
- 全局队列只用于新 goroutine 的“兜底存放”,不用于运行中 G 的再分发
- work-stealing 发生在 P 之间,偷的是其他 P 的
runq,不是sched.runq - netpoll 唤醒的 G 也不会进全局队列,而是直接推入目标 P 的本地队列或
runnext
全局队列什么时候会被写入?
新 goroutine 创建时,若当前 P 的本地队列已满(上限 256 个),runtime 会把本地队列一半的 G(约 128 个)挪到 sched.runq,再把新 G 放进本地队列——这是唯一常规写入路径。注意:不是“满了就全扔进去”,而是“挤出一半腾位置”,保证本地队列始终有容量,也避免全局队列瞬间暴涨。
容易踩的坑:go func() { ... }() 在短时间高频调用(比如循环启动 1000 个)时,前 256 个进本地队列,第 257 个触发迁移,之后每新增一个都伴随一次“搬 128 个 G 到全局队列”的开销,锁争用陡增,性能骤降。
- 没有 API 或配置能手动往
sched.runqpush G - GC 扫描、sysmon 等后台任务创建的 goroutine 也走同一套逻辑,同样受 256 限制
- 全局队列长度无硬上限,但过长(如 >10k)往往意味着 P 数量严重不足或存在调度阻塞
如何观察全局队列是否成了瓶颈?
用 runtime.ReadMemStats 看 NGCCPUFraction 没用,得看调度统计。最直接的方式是启用调度跟踪:GODEBUG=schedtrace=1000,每秒输出一行,其中 gqueue 字段就是 sched.runq.len。持续 >100 且不下降,基本可判定全局队列积压。
另一个信号是 runtime.GC() 后 goroutine 启动变慢——因为 GC 期间会暂停所有 P,新 goroutine 全被压进 sched.runq,恢复后要靠那 1/61 的机会慢慢捞。
-
go tool trace中搜索 “ScheduleGoroutine” 事件,看 G 是从runq还是sched.runq被 pick up 的 - 不要依赖
pp.runqhead == pp.runqtail判断空闲——本地队列空 ≠ 没活干,可能正卡在等全局队列轮询 - 在
GOMAXPROCS=1场景下,全局队列几乎等于“延迟放大器”,应尽量避免
真正影响调度及时性的,从来不是全局队列有多大,而是你有没有让足够多的 P 处于活跃状态——P 数太少,本地队列永远吃不饱,全局队列再大也救不了调度延迟。别盯着 sched.runq 优化,先确认 GOMAXPROCS 是否合理、是否存在长时间阻塞的系统调用拖住 M。


















