商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > Go 语言中 runtime 调度器中全局队列与本地队列的区别

Go 语言中 runtime 调度器中全局队列与本地队列的区别

  发布于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 里,内存访问延迟自然就低了。

Go 语言中 runtime 调度器中全局队列与本地队列的区别

全局队列(globalRunq)加锁但不可替代

全局队列是所有 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 饿死。
  • 让全局队列始终保有新鲜任务,大幅提升了 work stealing 的成功率。
  • 迁移本身是批量原子操作,比逐个 push 到全局队列高效得多。

你可以用 go tool trace 观察 Proc/RunQueueGlobal/RunQueue 的长度波动。正常服务中,二者应该是弱相关震荡的状态,而不是单边持续增长。

什么时候会从全局队列取任务?

触达 globalRunq 的 pop 操作,只有三种明确路径:

  • findrunnable() 函数中,当前 P 本地队列为空,并且所有其他 P 的本地队列都窃取失败后,才会尝试从 globalRunq 取一批(通过 globrunqget)。
  • 系统调用返回时,如果 M 找不到空闲的 P,它就把刚唤醒的 G 放进 globalRunq,等别的 M 来消费。
  • GC stw 阶段结束后,部分被暂停的 G 会统一注入 globalRunq,这是为了避免集中唤醒压垮单个 P

顺便说一句:别指望靠增大 GOMAXPROCS 来“减少全局队列的使用”。GOMAXPROCS 只管 P 的数量,不影响队列选择逻辑。真正影响全局队列压力的,是 goroutine 的创建节奏、阻塞比例以及 P 负载分布的均匀程度。

本文转载于:https://www.php.cn/faq/2398659.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注