直接回答:Go 1.14 之前 goroutine 的抢占是协作式的:调度器只能在函数调用入口(有栈扩张检查的地方)插入抢占点。一个纯计算的死循环(没有任何函数调用)可以永久霸占一个 P,STW 也会被它卡住——GC 无法开始,整个程序冻结。1.14 引入基于信号的异步抢占:sysmon 发现某 goroutine 运行超过 10ms,向对应线程发 SIGURG 信号,信号处理函数在安全点把该 goroutine 挂起并重新调度。

展开解析:选 SIGURG 是因为它不被用户程序使用且不会触发默认行为。安全点处理是关键难点:异步信号可能在任意指令到达,runtime 要判断当前 PC 是否处于可安全扫描的状态(寄存器里的指针是否已反映到栈上),不安全时设置标记延迟抢占;这也是偶尔看到 GC 延迟毛刺的来源之一。异步抢占还让 STW 有了硬保证:所有线程都能被信号拉到安全点,STW 时间从“最坏情况取决于用户代码”变成有上界,这对延迟敏感服务意义很大。关联演进:1.14 同期 defer 改为 open-coded 降低成本;非协作抢占让 tight loop 里不再需要显式 runtime.Gosched()。遗留边界:cgo 密集调用、超长无调用的内联展开循环仍可能影响调度及时性,排查调度延迟时 schedlatency 直方图(runtime/metrics)是第一手证据。

追问方向:什么是安全点(safepoint)?异步抢占对延迟敏感程序意味着什么?

(约 470 字)