发布于2026-07-10 阅读(0)
扫一扫,手机访问
先说说本地队列和全局队列的本质区别。很多人一听到“调度器”这个词,第一反应就是“队列”,然后下意识地把注意力放在了全局队列上。其实,在 Go runtime 的设计里,本地队列才是真正的“主角”,而全局队列更像是一个兜底的协调者。
本地队列之所以被优先使用,道理其实很直接:它是每个 P 独有的、无锁的 FIFO 队列。存取操作完全不涉及任何同步开销——这意味着只要本地队列非空,M 就能直接从里面弹出一个 G 来执行,整个过程都在 CPU cache 内完成,延迟极低。
有一个很常见的误解:有人觉得“全局队列更权威”,于是手动绕过了本地调度逻辑。结果呢?锁竞争和 cache line bouncing 反而让吞吐量降下来了。
P.runq 的容量上限是 256 个 G。一旦超过这个数,就会触发“半队列迁移”——把其中一半丢到全局队列里。G(比如用 go f())时,默认先进当前 P 的本地队列,而不是全局队列。G,它的栈内存和调度上下文大概率还待在当前 P 绑定的 CPU core 的 L1/L2 cache 里,内存访问延迟自然就低了。
全局队列是所有 P 共享的,带互斥锁。它可不是什么“备用通道”,而是调度器实现公平性与初始化语义的关键枢纽。
什么样的情况下才会用到它?
G 创建时,如果当前 P 不存在(比如在系统调用返回路径中创建),那就只能进全局队列了。G 从阻塞态(比如网络 I/O、定时器、channel recv)唤醒后,如果原来的 P 已经被其他 M 占用或者处于休眠状态,这个 G 也会被放进全局队列,等着重新绑定。P 的本地队列为空,而且也没能从其他 P 那里窃取到任务(work stealing)时,才退而求其次,从全局队列批量取一批——通常是 32 个。需要强调的是:globalRunq 上的锁(runqlock)是自旋锁+普通 mutex 的混合实现,争抢激烈时会短暂阻塞,但设计上已经把临界区长度压缩得相当短了。
这不是什么异常,恰恰相反,这是常态下主动的负载均衡机制。一旦 P.runq 达到 256,下一次新 G 进来时,runtime 会先把本地队列前 128 个 G 批量迁移到 globalRunq,再把新 G 塞进本地队列尾部。
很多人看到这个“迁移”动作,容易误以为这是某种性能劣化的信号。其实反过来想:
P 的队列无限膨胀,导致其他 P 饿死。你可以用 go tool trace 观察 Proc/RunQueue 和 Global/RunQueue 的长度波动。正常服务中,二者应该是弱相关震荡的状态,而不是单边持续增长。
触达 globalRunq 的 pop 操作,只有三种明确路径:
findrunnable() 函数中,当前 P 本地队列为空,并且所有其他 P 的本地队列都窃取失败后,才会尝试从 globalRunq 取一批(通过 globrunqget)。M 找不到空闲的 P,它就把刚唤醒的 G 放进 globalRunq,等别的 M 来消费。G 会统一注入 globalRunq,这是为了避免集中唤醒压垮单个 P。顺便说一句:别指望靠增大 GOMAXPROCS 来“减少全局队列的使用”。GOMAXPROCS 只管 P 的数量,不影响队列选择逻辑。真正影响全局队列压力的,是 goroutine 的创建节奏、阻塞比例以及 P 负载分布的均匀程度。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8