
Go 1.14+ 怎么触发 goroutine 抢占?
Go 从 1.14 开始支持真正的非协作式抢占,核心靠 sysmon 监控线程定期检查:只要某个 G 在同一个 P 上连续运行超过约 10ms,sysmon 就会向对应线程(M)发送 SIGURG 信号,强制中断当前执行,将其状态设为 _gpreempted,并放回运行队列。
这不是靠函数调用点“埋点”(如 morestack_noctxt)被动检测,而是异步、信号驱动的主动干预。但注意:抢占只发生在“安全点”,比如函数调用返回前、栈增长检查时、或 GC 扫描中——不是任意指令都能打断。
-
sysmon每 20ms 左右轮询一次所有P的schedtick和sysmontick,比对调度次数和时间戳来判断是否超时 - 抢占不等于立即切换:被抢占的
G进入全局队列或本地队列,下次调度时才可能被其他M拿走 - 纯计算循环(如
for { i++ })若无函数调用、无栈操作、无阻塞点,在 1.14–1.20 间仍可能逃逸抢占——直到 1.21 引入更激进的asyncPreempt插桩才大幅改善
为什么 runtime.preemptM 有时没效果?
常见现象是:开了 GODEBUG=asyncpreemptoff=0,也确认 sysmon 在跑,但某 goroutine 还是卡死 CPU 不让位。根本原因是抢占被绕过了。
runtime.preemptM 发送信号后,目标 M 必须在用户态且未屏蔽信号才能响应。以下情况会导致失效:
立即学习“go语言免费学习笔记(深入)”;
- 该
M正在执行系统调用(如read、epoll_wait),内核态无法收信号,需等返回用户态;而若 syscall 长期阻塞(如网络 hang),sysmon会直接回收其绑定的P,但原G仍卡住 -
G处于_Gsyscall或_Gwaiting状态(比如刚进 syscall 或在 channel 上等待),此时不满足抢占条件,preemptM会直接返回 false - CGO 调用期间,
M可能脱离 Go 调度器管理,信号 handler 不生效;且 Go 1.22 前默认禁用 CGO 调用中的抢占插桩 - 某些低层汇编函数(如
memclrNoHeapPointers)被标记为noinline+go:nosplit,跳过抢占检查
asyncpreempt 和 morestack 两种抢占路径怎么选?
Go 实际使用双轨抢占:同步路径靠 morestack 系列函数(函数调用时检查),异步路径靠 asyncpreempt(信号中断)。它们不是互斥,而是互补覆盖不同场景。
morestack 是传统方式,插入在每个函数序言中,开销小但依赖调用链;asyncpreempt 是新增的汇编 stub,由信号 handler 跳转执行,能打断长循环,但需额外栈空间和寄存器保存逻辑。
- 启用条件不同:
morestack全局生效;asyncpreempt默认开启,但可通过GODEBUG=asyncpreemptoff=1关闭 - 触发时机不同:
morestack只在函数入口检查(所以空循环不触发);asyncpreempt可在任何用户指令间隙插入(需 CPU 支持REX前缀重写等特性) - x86_64 上,
asyncpreempt会在每 ~10ms 的指令流中插入一个CALL asyncpreempt(实际是 patch 进去的),但仅限于 Go 编译生成的代码;手写汇编或 CGO 不受此影响
哪些代码容易被误认为“没抢占”,其实是设计使然?
开发者常抱怨“明明跑了 50ms 的 for 循环,为啥没被抢?”,然后怀疑调度器坏了。其实多数时候是符合预期的行为。
Go 调度器的抢占目标从来不是“精确 10ms 切换”,而是防止某个 G 长期霸占 P 导致其他 G 饿死。它容忍短时偏差,也刻意避免高频抢占带来的上下文切换开销。
- 抢占阈值不是硬实时:10ms 是平均值,
sysmon自身调度也有延迟,实际可能 12–15ms 才触发 - 抢占后不一定立刻执行别的
G:如果本地队列和全局队列都空,被抢占的G可能马上又被同个P拿起来继续跑 - GC STW 阶段会关闭抢占(避免干扰扫描),此时所有
G都可能被“豁免”,这是有意为之,不是 bug -
GOMAXPROCS=1时,抢占意义大幅下降——因为只有一个P,抢来抢去还是它自己调度
真正要警惕的,不是“偶尔没抢到”,而是大量 goroutine 集中在单个 P 上长期计算、无任何阻塞点,且系统响应明显变慢。这时得看是不是忘了加 runtime.Gosched(),或者该拆成带 channel/定时器的协作式结构了。










