runtime.gosched() 是唯一可控的显式让出点,用于纯原子循环等绕过调度安全点的场景,防止 goroutine 饿死其他任务;它仅协作式让出 cpu,不休眠、不释放锁、不保证内存可见性。

Go 的 runtime 调度器不靠你“手动调度”,它自动运行;你真正要做的,是别写出绕过安全点的代码——比如纯计算循环,否则其他 goroutine 会饿死。
findrunnable() 怎么挑出下一个 goroutine 执行
findrunnable() 是调度循环里唯一负责“找活干”的函数,它按固定优先级扫描,顺序不能跳过:
- 先看
pp.runnext:这是为抢占预留的单个高优先级 G,命中就直接返回,不走后续流程 - 再查
pp.runq(P 的本地队列):无锁、最快路径,90% 以上的 goroutine 从这里被捞出 - 本地队列空时,以
pp.schedtick % 61 == 0的概率检查全局队列sched.runq:防新 goroutine 饿死,但太频繁会争抢锁 - 接着尝试从其他 P 的本地队列“偷任务”(
runqsteal()):每个 P 随机选一个目标 P 尝试偷一半任务,负载均衡靠这个 - 最后调用
netpoll():唤醒因网络 I/O 就绪而挂起的 G,比如conn.Read()返回后注册的回调
注意:findrunnable() 是阻塞函数——如果以上全空,当前 M 会进入自旋或休眠,直到有新任务到来。
for {} 纯计算循环为什么卡死整个 P
这类循环不触发任何安全点:
- 没函数调用 → 绕过抢占检查
- 不分配内存 → 不进堆栈扩容逻辑
- 不碰 channel / net / time → 不进 I/O park 路径
-
atomic原语是内联汇编,完全不经过 runtime 插桩
结果就是:这个 G 一直霸占所属 P,其他所有 goroutine(包括 main 和 fmt.Println)都拿不到执行机会。Go 1.14+ 的 SIGURG 抢占默认每 10ms 才触发一次,且只在安全点生效,所以仍可能造成可观延迟。
runtime.Gosched() 在什么场景下真有用
runtime.Gosched() 不是 sleep,也不挂起 goroutine,它只是把当前 G 放回本地队列尾部,强制让出当前 M 的控制权。它只在极少数场景下必要:
- 手写自旋等待逻辑,且无法引入 channel(如底层驱动、嵌入式环境)
- 轮询某个共享变量,又不能加
atomic.Load*(但你应该加) - 教学演示或调试极端调度竞争问题
常见误用:for i := 0; i —— 高频调用等于主动制造调度抖动,且不解决内存可见性问题。若变量 <code>done 是普通 bool,for !done { runtime.Gosched() } 可能永远不退出,正确做法是 atomic.LoadBool(&done)。
异步抢占(SIGURG)为什么有时不生效
SIGURG 抢占不是随时打断,它依赖安全点,且受多重约束:
- sysmon 每约 20ms 轮询,只对状态为
_Prunning且连续运行 ≥10ms 的 P 触发preemptone - 若目标 M 的 signal mask 屏蔽了
SIGURG(比如在 CGO 调用中、或显式调用了runtime.LockOSThread()),信号直接丢弃,不重试 - 在
//go:nosplit函数内、GC 栈扫描临界区、runtime.mallocgc内部长时间运行等场景,抢占会被跳过 - 纯寄存器循环(如
for i := 0; i 被优化成无内存访问)可能逃逸所有安全点
最容易被忽略的是:抢占失败往往不是调度器坏了,而是信号根本没送达——得用 strace -e trace=rt_sigprocmask,rt_sigaction,kill 实际抓证据,而不是靠现象猜测。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











