go runtime调度器自动运行,开发者只需避免写出绕过安全点(如纯计算循环)导致抢占失效的代码;findrunnable()按优先级依次检查runnext、本地队列、全局队列、偷任务和netpoll,全空时阻塞等待。

runtime 调度不是你要“手动做”的事,而是 Go 自动运行的机制;你真正要做的,是别写出让它失效的代码。
findrunnable() 怎么挑 goroutine 出来执行
findrunnable() 是调度循环里唯一负责“找活干”的函数,它按固定优先级扫描资源,顺序不能跳过:
- 先看
pp.runnext:这是为抢占预留的单个高优先级G,命中就直接返回,不走后续流程 - 再查
pp.runq(P 的本地队列):无锁、最快路径,90% 以上的 goroutine 从这里被捞出 - 本地队列空时,以
pp.schedtick % 61 == 0的概率检查全局队列sched.runq:防新 goroutine 饿死,但太频繁会争抢锁 - 接着尝试从其他 P 的本地队列“偷任务”(work-stealing):每个 P 会随机选一个目标 P 尝试偷一半任务,负载均衡靠这个
- 最后调用
netpoll():唤醒因网络 I/O 就绪而挂起的G,比如conn.Read()返回后注册的回调
注意:findrunnable() 是阻塞函数——如果以上全空,当前 M 会进入自旋或休眠,直到有新任务到来。
for {} 循环为什么卡死整个 P
纯计算循环(如 for {}、for i := 0; ; i++ { atomic.AddUint64(&x, 1) })不触发任何安全点:
- 没函数调用 → 绕过抢占检查
- 不分配内存 → 不进堆栈扩容逻辑
- 不碰 channel / net / time → 不进 I/O park 路径
-
atomic原语是内联汇编,完全不经过 runtime 插桩
结果就是:这个 G 一直霸占所属 P,其他所有 goroutine(包括 main 和 fmt.Println)都拿不到执行机会。
Go 1.14+ 虽引入基于 SIGURG 的异步抢占,但默认每 10ms 才检查一次,且受限于安全点位置——循环体内没调用,信号来了也得等下一个函数返回才能响应。
runtime.Gosched() 什么时候真该用
绝大多数场景根本不需要它。以下情况才考虑加:
- 手写自旋等待,且无法用
sync.Cond或带default的select替代 - 遍历超大 slice 做密集计算(例如校验千万条记录),且循环体里既没函数调用,也没内存操作或原子操作以外的副作用
- 明确知道某段逻辑会长期占用 CPU(>1ms),又必须保留在单个 goroutine 中(比如嵌入式实时控制)
runtime.Gosched() 只是把当前 G 放回本地队列尾部,不休眠、不释放锁、不触发 GC,下一轮仍可能立刻被选中。频繁调用反而制造调度抖动;建议按批次控制,比如每处理 10000 次后调用一次。
别在 for { select {} } 里加它——这仍是死循环,应改用 time.Sleep(0) 或带 default 的 select。
runtime.GOMAXPROCS 设成 1 会发生什么
runtime.GOMAXPROCS(1) 不是“限制并发数”,而是把可用 P 锁死为 1 个:
- 所有 goroutine 必须排队等这唯一的 P 空出来才能运行
- 即使机器有 64 核,
go f()和go g()也会严格串行执行 - 网络 I/O、系统调用仍能并发(因为 M 可解绑),但 CPU 密集型任务彻底失去并行能力
这不是调试手段,而是明确的性能降级配置。除非你在跑单核嵌入式设备,或做确定性测试,否则不要设为 1。
真正的瓶颈往往不在 P 数量,而在 goroutine 是否写了不可调度的纯计算逻辑——这点比调参数重要得多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











