Go: 抢占式调度
1 · Go: 抢占式调度#
runtime 为了避免 goroutine 长时间独占 CPU,会在 safe point 把它暂停并交回调度器。
1.1 · 协作式
goroutine自己让出 CPU。
常见时机:
- 主动调用
runtime.Gosched() - I/O、syscall、channel、锁阻塞
- 进入 runtime 明确安排的调度点
问题:纯计算或长循环如果不主动让,别的 goroutine 可能饿死。
1.2 · 抢占式:同步
runtime 决定“该让了”,但要等 goroutine 到下一个函数序言的栈检查点。
做法:复用函数序言里的栈扩容检查。runtime 把 stackguard0 设为 stackPreempt,让 goroutine 在下一次栈检查时进入 morestack/newstack,再切回调度器。
- 把
stackguard0设为stackPreempt - 等 goroutine 进入函数序言的栈检查
- 借
morestack/newstack路径切回调度器
本质:runtime 要它让,不是 goroutine 自愿让。
1.3 · 抢占式:异步
Go 1.14 引入,解决 tight loop 长时间不到函数调用点的问题。
做法:
- runtime 给承载该 goroutine 的线程(M)发信号
- 信号处理器检查当前位置是不是 async safe point
- 如果安全,转入
asyncPreempt保存现场,再走抢占路径切回调度器
本质:比同步抢占更主动,但也不是任意位置都能打断。
1.4 · 安全点 safe point#
抢占不能发生在任意指令位置,只能发生在 safe point。
原因:runtime 要保证栈、寄存器、GC 状态可安全处理。
1.5 · 对比
- 协作式:goroutine 自己让
- 同步抢占:runtime 要它让,等到同步 safe point
- 异步抢占:runtime 发线程信号,尽量在 async safe point 停下来
可以使用 GODEBUG=asyncpreemptoff=1:关闭异步抢占
2 · links#
- https://unskilled.blog/posts/preemption-in-go-an-introduction/
- https://github.com/gopherchina/conference/blob/master/2021/2.2.3%20Go%E8%AF%AD%E8%A8%80%E7%9A%84%E6%8A%A2%E5%8D%A0%E5%BC%8F%E8%B0%83%E5%BA%A6.pdf
- https://hidetatz.github.io/goroutine_preemption/
- https://tip.golang.org/doc/go1.14#runtime 1.14引入
- https://go.googlesource.com/proposal/+/master/design/24543-non-cooperative-preemption.md