sysmon每10ms检查两类信号:一是g在同一个p上连续运行≥10ms,二是p处于系统调用且阻塞≥20us;检查后仅设preempt标志,真抢占发生在函数返回、循环边界等安全点。

Go 的抢占不是“随时打断”,而是有明确触发条件和延迟窗口的被动机制——它不会在任意指令点中断 goroutine,而依赖 sysmon 线程每 10ms 检查一次运行超时的 G。
sysmon 线程每 10ms 检查什么?
sysmon 是一个独立的、不绑定 P 的 M,它周期性轮询所有正在运行的 G,但只关注两类信号:
- 当前 G 在同一个 P 上连续运行时间 ≥
10ms(硬阈值,不可配置) - P 处于系统调用中且阻塞时间 ≥
20us(用于回收空闲 P)
注意:这个 10ms 不是从 go func() 启动开始计时,而是从 G 被调度到 M 上执行起,持续占用 CPU 的时长。中间若有 channel 阻塞、syscall、GC 栈扫描等让出行为,计时会重置。
抢占标记 ≠ 立即切换
sysmon 发现超时 G 后,只会设置 G.preempt 标志位,并不直接切换上下文。真正的抢占发生在下一次“安全点”:
- 函数调用返回前(ret 指令处)
- for 循环迭代边界(如
i++后的条件判断) - goroutine 主动调用
runtime.Gosched()
这意味着:纯计算型循环(无函数调用、无分支跳转)可能跑满整个时间片才被抢;而带频繁函数调用的逻辑,往往在几毫秒内就被打断。这也是为什么 for { i++ } 容易导致调度延迟,而 for { time.Sleep(1) } 几乎不会。
stackMark 与栈安全抢占的矛盾
Go 1.14 引入基于栈扫描的抢占(stackMark),目标是解决纯计算 loop 的饥饿问题,但它依赖“栈可安全遍历”这一前提:
- 若 G 正在执行
runtime.makeslice或runtime.growslice等栈操作,此时栈结构不稳定,stackMark会被跳过 - 抢占失败后,该 G 会继续运行,直到下一个 sysmon 周期或下一个安全点
- 因此,极端情况下(如密集内存操作 + 无调用点),单个 G 可能连续占用 P 达数十毫秒
这不是 bug,而是设计取舍:宁可容忍短暂饥饿,也不愿因强制栈扫描引发 panic 或数据损坏。
如何验证你的 goroutine 是否被抢占?
最直接的方式是启用调度追踪:
go run -gcflags="-l" -ldflags="-s" -v -trace=trace.out main.go go tool trace trace.out
在浏览器打开 trace 页面后,重点关注:
- 每个 G 的“Running”段是否被频繁截断(长度 ≤ 10ms)
- 是否存在长时间(>15ms)未中断的 Running 块 —— 这大概率是抢占失效点
- 对应 P 的状态是否长期为 “Running”,而其他 P 处于 “Idle”
真正难调的不是“没抢占”,而是“抢占了但没效果”:比如被抢的 G 立刻又被调度回原 P,或者窃取来的 G 立即陷入 syscall。这些需要结合 pprof 的 goroutine 和 schedule profile 综合判断。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











