runtime.gosched在go 1.14+中基本失效,因调度器已支持异步抢占;仅在极少数自旋等待且无法使用channel等标准同步原语的底层场景下才可能用到。

runtime.Gosched 在 Go 1.14+ 中基本失效
它现在几乎没用。Go 从 1.14 开始启用基于信号的异步抢占,调度器能主动中断长时间运行的 goroutine(比如大数组遍历、密集计算循环),不再依赖你手动插入 runtime.Gosched()。你写个 for {} 死循环,调度器大概率会在 10ms 内强行切走——除非你关掉抢占(GODEBUG=asyncpreemptoff=1),但那只是测试用途。
唯一还可能用到它的场景:手写自旋等待且无法用 channel
当你要轮询一个共享变量(比如某个 sync/atomic 标志位),又不能用 select 或无缓冲 chan 等待时,runtime.Gosched() 才是最后手段:
- 你不能改用
time.Sleep(1 * time.Nanosecond)(它语义更可靠,但有纳秒级休眠开销) - 你不能加
sync.Cond或sync.WaitGroup(比如在极底层同步原语实现中) - 你确认这段代码跑在单个 P 上,且没有其他 goroutine 可调度(否则
Gosched只是白调)
示例(不推荐,仅说明逻辑):
var ready int32
go func() {
time.Sleep(time.Millisecond)
atomic.StoreInt32(&ready, 1)
}()
for atomic.LoadInt32(&ready) == 0 {
runtime.Gosched() // 不加这行,可能卡死;加了,也不保证立刻看到 ready 更新
}
注意:这行代码不解决内存可见性——你仍需 atomic.LoadInt32,不能直接读 ready。
常见错误:把它当 time.Sleep 或 channel 替代品
runtime.Gosched() 不会让当前 goroutine 进入等待队列,也不触发公平调度。它只是“这次不跑了”,下一轮仍可能立刻被选中。所以这些做法都错:
- 用
runtime.Gosched()替代time.Sleep(0):新版 Go 中time.Sleep(0)已优化为轻量让渡,语义更明确 - 在纯计算循环里每轮都调
runtime.Gosched():调度抖动明显,且现代调度器本就会抢,反而干扰自动判断 - 以为调了它就能“等其他 goroutine 先跑完”:它不阻塞、不挂起、不排队,只是把当前 goroutine 插回就绪队列尾部
真正该用什么?优先选标准同步原语
绝大多数需要“让出 CPU”的真实需求,都有更安全、更语义清晰的替代方案:
- 等条件满足 → 用
sync.Cond或带超时的select+time.After - 控制执行节奏 → 用
time.Sleep(哪怕1 * time.Nanosecond) - 协调生产者/消费者 → 用带缓冲或无缓冲的
chan - 避免饿死 → 调整
GOMAXPROCS,或拆分计算任务为多个 goroutine
手动让渡是边缘中的边缘操作。它不解决竞态、不修复可见性、不改善公平性,只在极少数你完全掌控调度上下文、且其他所有同步机制都被排除的情况下,才值得考虑——而那种情况,你大概率已经在写 runtime 层代码了。











