跳转到主内容
websoft网络软件专家 - 深耕网络技术,打造实用软件!

为什么 Golang 1.14 引入了抢占式调度

Go 1.14+虽引入异步抢占,但纯空循环仍可能逃逸:因无安全点、内联优化、CGO/LockOSThread屏蔽信号、Windows限制或GODEBUG禁用等,需用runtime.Gosched()兜底并用schedtrace/trace工具验证。 纯计算循环会卡死整个 P,1.14 之前根本没法救 Go 1.13 及更早版本的调度器是协作式的:goroutine 必须主动调用
runtime.Gosched()
、进入系统调用、操作 channel 或调用函数,才能让出 CPU。但像
for {}
或
for i := 0; i 这类纯算术循环,不触发任何协作点,就会让绑定的 P 长期霸占线程(M),其他 goroutine 彻底“饿死”——哪怕你启了 100 个打印日志的 goroutine,它们也一个都跑不起来。
这不是性能差的问题,是功能缺失:你写了个死循环调试逻辑,整条 P 就废了,GC 都可能被拖住,
pprof
看到的全是单个 G 持续
running
超过数秒甚至分钟。 SIGURG 信号 + 安全点 = 第一次能“硬拽下来” Go 1.14 引入基于
SIGURG
的异步抢占,核心不是“每 10ms 切一刀”,而是:
sysmon
每 20ms 左右轮询,发现某个 G 在 P 上连续运行超约 10ms → 调用
signalM
向对应 M 发送
SIGURG
→ M 在下一个安全点(如函数返回前、栈检查处)响应信号,把当前 G 状态设为
_gpreempted
,放回队列。 它不依赖你有没有写函数调用,只要指令流里存在编译器插入的安全点(1.14 默认开启,可用
go run -gcflags="-S" main.go 2>&1 | grep preempt
确认) Linux/macOS 支持完整机制;Windows 因信号模型限制,效果弱很多 抢占后 G 不一定立刻再被调度,只是“有资格排队”,下次
scheduler
拿到 P 时才可能选中它 为什么空循环 still sometimes slips through 即便在 1.14+,
for {}
仍可能逃逸抢占,常见原因: 立即学习 “ go语言免费学习笔记(深入) ”; go语言参考手册 中文CHM版 Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。本文给大家带来Go参考手册,需要的可以来下载! Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。 Go 语言特色 简洁、快速、安全 并行、有趣、开源 内存管理、v数组安全、编译 下载 循环体被内联且完全无函数调用、无栈操作(如
i++
、
a += b * c
),导致没有安全点可插桩——1.20 前尤其明显,1.21 加强了插桩才大幅缓解
runtime.LockOSThread()
或 CGO 调用期间,M 脱离 Go 调度器管理,
SIGURG
被屏蔽或 handler 不生效 目标 M 正陷在不可中断的系统调用(D 状态)或执行原子指令(如
LOCK
前缀),信号被丢弃 环境变量设了
GODEBUG=asyncpreemptoff=1
,或容器里禁用了
SIGURG
这类场景下,
runtime.Gosched()
不是“过时技巧”,而是唯一可控的保险丝——它语义明确、开销低、不依赖信号送达,比如在 hot loop 里写
if i%1024 == 0 { runtime.Gosched() }
就很稳。 验证抢占是否真在工作,别靠感觉 “程序好像没卡住”不能说明抢占生效。真实验证方式只有三种: 加
GODEBUG=schedtrace=1000
运行,观察输出中
gwait
是否持续上涨(涨 = goroutine 堆积未被调度) 用
go tool trace
:启动时启用
runtime/trace.Start
,打开 trace 页面后搜
Preempted
事件,或看某 G 的
running
区段是否被切成多段(中间夹着
runnable → running
) 写对照测试:一个
for {}
goroutine + 5 个
time.Sleep(1 * time.Millisecond); fmt.Println(time.Now())
,如果后者能在几毫秒内轮流打印,说明抢占基本在线 真正容易被忽略的是信号链路本身:
sysmon → signalM → tgkill → SIGURG → M 用户态响应
,任一环节断掉(比如 C 代码调用
pthread_sigmask
后没恢复),抢占就静默失效——这时
strace -p -e trace=tgkill,sigprocmask
才是第一排查手段。

相关文章