GO并发的runtime.Gosched 有什么用(结论:没卵用了)

张开发
2026/4/18 1:31:19 15 分钟阅读

分享文章

GO并发的runtime.Gosched 有什么用(结论:没卵用了)
runtime.Gosched 这个有什么用简单来说runtime.Gosched()的作用是主动让出 CPU 时间片。 [1, 2]想象一下你在排队买饭轮到你时你突然发现还没想好吃什么于是你对后面的人说“你先买吧我回队尾重新排”。这就是runtime.Gosched()做的事。核心功能与特点暂停但不阻塞它会暂停当前的 Goroutine将其放回就绪队列Wait Queue的末尾等待下一次调度。它不会像time.Sleep那样把协程挂起进入睡眠状态当前协程随时可能再次被执行。配合协作式调度Go 的调度器在某些情况下是“协作式”的。如果一个协程在进行大量的纯计算比如一个死循环for {}它可能会一直霸占着 CPU 不放导致其他协程“饿死”。调用Gosched可以手动给其他协程运行的机会。 [3, 4, 5, 6]常见使用场景防止长循环霸占 CPU在执行耗时非常长的纯逻辑计算且没有 I/O 或系统调用时手动插入Gosched确保程序整体的响应性。测试并发竞争有时候在写单元测试时为了更容易触发并发下的竞态问题会用它来增加协程切换的概率。自旋锁改进在实现底层的同步原语如自旋锁时如果获取不到锁可以调用它来减少无谓的 CPU 消耗。 [4, 7, 8, 9]为什么现在很少见到它在旧版本的 Go 中这种手动让出非常重要。但从 Go 1.14 开始Go 引入了基于信号的抢占式调度。这意味着即使你的协程在跑死循环调度器也能强行把它切下来。因此现代 Go 开发中几乎不需要显式调用runtime.Gosched()调度器已经足够聪明了。 [4, 10]你想了解一下在 Go 1.14 之后即使没有这个函数调度器是如何强行中断“霸道”协程的吗他让多久呀它没有固定的时间。runtime.Gosched()并不是像time.Sleep那样设定一个闹钟。它的逻辑是“我不做了现在立刻把机会让出来请调度器安排其他人把我排在最后就行。”具体能“休息”多久取决于以下两个因素队列长度在当前调度器P的本地运行队列里还有多少个其他的 Goroutine 在排队如果有 10 个你就得等那 10 个跑完或者被切走如果没有其他人你可能下一毫秒又被拉回来继续跑。调度策略Go 调度器会扫描全局队列和其他 P 的任务。如果有更紧急的任务你等的时间就长如果大家都很闲你几乎是瞬间“重回王位”。形象比喻time.Sleep(2s)你跟老师请假“我肚子疼请假 2 小时这期间谁也别叫我我睡醒了再回来。”runtime.Gosched()你排队买奶茶轮到你时你说“我等个朋友我先去队尾重新排。”总结它让出的不是“时间”而是“执行权的顺位”。如果后面没人在排队它可能转头又开始执行了。你是因为遇到了某个 Goroutine 抢占了太多资源所以想用这个函数来优化性能吗关键是他要等多久的朋友,要让多少个后面的人这正是runtime.Gosched()最“佛系”的地方它不等人也不管后面有多少人它只管执行“归队”这个动作。用奶茶店的例子深挖一下你会发现它极其机械化1. 它要等多久答案0秒。它不会在旁边站着等朋友变量或资源它说完成那句“我去队尾”之后瞬间就已经站在队伍最后面了。它不需要等任何外部条件触发只要轮到它它就能继续。2. 要让多少个后面的人答案让完当前“本组”队列里的所有人。在 Go 的底层调度模型GMP中每个人Goroutine排在不同的“窗口”P处理器。如果你后面排了 3 个人你就让这 3 个人先买你排在第 4 个。如果你后面 没人你刚走到队尾发现前面空空如也于是你马上又回到了柜台前。关键细节它“认死理”runtime.Gosched()只负责让出当前的执行机会它有以下两个特点不保证公平如果其他协程都在忙着做别的事比如在睡觉time.Sleep或者在等网络 I/O哪怕你执行了Gosched调度器看了一圈发现没人能干活还是会把你拉回来。只看“本地队列”Go 的每个处理器都有一个本地队列最多 256 人。通常你只是让给这 256 个人里的“待命者”而不是让给全公司全局队列的所有人。总结你不需要担心它“等太久”它更像是一种姿态“我不霸占资源如果有人要用 CPU你们先来如果没有我接着干。”如果你其实是想让某个协程“精准等待”另一个协程完成任务那你需要的可能不是Gosched而是Channel或者WaitGroup。需要我帮你对比下这几种方式的区别吗一般什么场合会用到他在现代 Go 开发中由于 Go 1.14 引入了抢占式调度即使你不主动让调度器也会强行切断你的长任务显式使用runtime.Gosched()的场景已经非常少见了。但在以下 4 个特定场合它依然有不可替代或独特的价值1. 紧凑死循环中的“人道主义”虽然有抢占调度但抢占是基于“异步信号”的约 10ms 检查一次。如果你的程序里有一个执行极快的死循环比如不断的位运算为了确保其他微小的协程能更丝滑、更低延迟地被唤醒可以手动加一个。for !condition { runtime.Gosched() // 不想让 CPU 烧得太热也给别人一点机会 }2. 实现“自旋锁”Spinlock在底层开发比如自己写高性能同步原语时如果获取不到锁直接进入阻塞Sleep/Channel开销太大。通常会先执行几次“自旋”尝试。如果自旋几次还没拿到调用Gosched比让协程直接进入“睡眠状态”更轻量。逻辑 认为锁很快就会释放所以先排到队尾等一会儿回来再看而不是直接回家睡觉。3. 测试并发下的“竞态条件”Race Condition这是开发者用得最多的场景。在写测试用例时为了复现某个隐藏很深的并发 Bug我们会故意在关键逻辑处插入Gosched。目的 人为制造“执行间隙”强制触发上下文切换从而增加不同协程交叉执行的概率让原本难以复现的 Bug 露头。4. 协作式逻辑的“礼让”当你有一组 Goroutine 在协作执行任务其中一个协程发现自己目前的条件还没完全准备好但又没到需要Wait或Block的地步它可以礼貌性地让出位置让其他协程先推进进度。⚠️ 注意不要把它当成控制工具如果你发现自己是为了以下目的而想用它那通常是用错了为了等待某个结果请使用channel。为了让某个协程先运行请使用sync.WaitGroup。为了定时请使用time.Sleep或time.Timer。总结Gosched是一剂“助推剂”它不保证逻辑正确性只提供调度上的微调。你是因为在调试并发 Bug 时发现逻辑顺序不对所以考虑用它吗或者是看到了某些底层库源码里有这个用法我只是看文档发现他这个让出时间太模糊不像WG有明确的wait你的直觉非常敏锐。Gosched是“建议性”的而WaitGroupWG是“强制性”的。 这种模糊感正是因为它们设计的维度完全不同1. 维度的区别逻辑层 vs. 调度层WaitGroup (WG) 是“逻辑同步”它关心的是任务做没做完。比如“我要等 5 个文件全部下载完才能开始压缩”这是一种业务逻辑上的硬性先后顺序。Gosched 是“调度优化”它关心的是CPU 忙不忙。它不关心里面的任务执行到哪了只关心“我占着茅坑太久了换个人来蹲会儿”。2. 为什么Gosched必须模糊因为它面对的是 不确定性不知道后面有多少人调度队列里可能有 0 个协程也可能有 1000 个。不知道别人要跑多久它让给后面的人后面的人可能运行 1 微秒就结束了也可能运行 10 毫秒才交还权力。不知道 CPU 有几个内核在多核环境下你让出的只是当前这颗核的执行权别的核可能还在并行跑着。所以Gosched永远无法给你一个“明确的时间”。3. 场景对比什么时候该选谁特性runtime.Gosched()sync.WaitGroup你的需求“我累了谁想先跑谁跑没人跑我继续。”“我必须等那几个兄弟干完活我才动。”确定性极低可能瞬间又轮到你极高Done 没减到 0 绝不醒来资源消耗极低仅一次上下文切换低涉及信号量和协程挂起/唤醒底层状态Runnable就绪态还在队里Waiting阻塞态移出队列总结文档写得模糊是因为Gosched的本质就是 “随缘”。如果你需要确定性比如 A 一定要在 B 之后执行绝对不能用Gosched必须用WaitGroup或Channel。如果你只是想做一个“好公民”不希望自己的计算任务把单核 CPU 拉满导致系统卡顿才考虑Gosched。在 Go 的世界里“显式优于隐式”。既然你觉得它模糊那大部分时候你都不需要它直接上Channel或WaitGroup才是正道。你最近是在实现什么高并发的任务系统吗如果是涉及到大量小任务的同步我可以帮你看看用哪种通信模式更稳妥。结论就是Go 1.14 版本之后他现在没什么用了也很少有人用他了完全正确。 你的理解非常到位。在 Go 1.14 版本之后它确实从“核心工具”退化成了“边缘工具”。两个核心原因让它变得“没那么有用”了抢占式调度Preemptive Scheduling以前如果你的协程写了个死循环for {}它能把整个 CPU 核心锁死必须靠Gosched救命现在 Go 编译器会自动在函数调用里插入抢占点或者通过系统信号强制把霸占 CPU 的协程踢下来。更成熟的并发原语现代开发中大家更习惯用Context、Channel和WaitGroup。这些工具能清晰地表达“我要等多久”或“我因为什么而等”而不是像Gosched那样全凭运气。现在的它 绝大多数 Gopher 哪怕写一辈子代码可能都不会在生产环境里手动敲一行runtime.Gosched()。它现在更多地出现在 Go 运行时源码、底层网络库或者极端性能调优的实验室代码里。

更多文章