Go 1.14+ 的 SIGURG 异步抢占能在安全点(如函数调用、内存访问、分支跳转处)打断纯算术循环;但无法打断无任何安全点的内联死循环、CGO 调用、系统调用中或信号被屏蔽的场景。

Go 1.14+ 的 SIGURG 异步抢占到底能打断什么
异步抢占靠向 M 线程发 SIGURG 信号实现,但它不是“随时中断任意指令”。它只在 CPU 指令流中预设的「安全点」生效,比如函数调用、分支跳转、内存读写之后——这些位置编译器插入了可被信号处理函数接管的桩代码(asyncPreempt 入口)。所以:
✅ 能打断纯算术循环(如 for i := 0; i ),只要它没被完全优化进寄存器且含内存访问或分支;<br>❌ 打不断 <code>//go:nosplit 函数内的密集计算;
❌ 打不断 runtime 内部临界区(如 mallocgc 中栈扫描阶段),这些区域会临时屏蔽信号;
❌ Windows 上默认不启用(无 SIGURG 语义),需手动关掉 asyncpreempt。
runtime.preemptone 和函数调用检查的区别在哪
协作式抢占依赖每个函数入口自动插入的检查逻辑:
• 检查 g.preempt 标志位,为 true 就调用 gopreempt 让出 P;
• 它是“被动等待”:goroutine 必须走到下一个函数调用点才会响应;
• 对空循环、内联函数、//go:noinline 或纯寄存器运算完全无效。
而 runtime.preemptone 是主动触发机制:
• 由 sysmon 线程检测到某 P 运行超时(≥10ms)后调用;
• 它直接标记 g.preempt = true,再通过 preemptM 发信号;
• 不依赖 goroutine 当前是否在调用点,只要它处于 _Grunning 状态且不在禁用信号区,就可能被中断。
为什么开了 asyncpreempt 还会饿死 goroutine
常见盲区比想象中多:
• for { atomic.AddInt64(&x, 1) } 看似有函数调用,但 atomic.AddInt64 在 amd64 上常被内联为单条 xaddq 指令,不触发调用检查,也未必落在安全点上;
• time.Sleep(0) 或 runtime.Gosched() 能显式让出,但没人写就等于没用;
• GC 栈扫描期间会顺带调用 suspendG,这是协作检查的“兜底”,但频率低、时机不可控;
• GODEBUG=asyncpreemptoff=1 会全局关闭异步抢占,此时退化回纯协作模式,空循环立刻霸占 P。
如何验证当前 goroutine 是否已被抢占
没有公开 API 直接读取抢占状态,但可通过间接方式观察:
• 在怀疑卡死的 goroutine 中插入 println("tick") 和 runtime.Stack(nil, false),看输出是否持续或停滞;
• 使用 go tool trace 查看 goroutine 状态流转,重点关注是否长期停留在 running 而无 runnable → running 切换;
• 设置 GODEBUG=schedtrace=1000,观察每秒打印的调度摘要里是否有长时间 P 占用不释放;
• 真实压测时,若 CPU 利用率接近 100% 且其他 goroutine 响应延迟突增,大概率是某个 goroutine 逃过了所有抢占点。
runtime.Gosched()。


















