Go runtime 调度器异步抢占是啥?
摘要:原创文章,欢迎转载,转载请注明出处,谢谢。 0. 前言 前面介绍了运行时间过长和系统调用引起的抢占,它们都属于协作式抢占。本讲会介绍基于信号的真抢占式调度。 在介绍真抢占式调度之前看下 Go 的两种抢占式调度器: 抢占式调度器 - Go 1
原创文章,欢迎转载,转载请注明出处,谢谢。
0. 前言
前面介绍了运行时间过长和系统调用引起的抢占,它们都属于协作式抢占。本讲会介绍基于信号的真抢占式调度。
在介绍真抢占式调度之前看下 Go 的两种抢占式调度器:
抢占式调度器 - Go 1.2 至今
基于协作的抢占式调度器 - Go 1.2 - Go 1.13
改进:通过编译器在函数调用时插入抢占检查指令,在函数调用时检查当前 Goroutine 是否发起了抢占请求,实现基于协作的抢占式调度。
缺陷:Goroutine 可能会因为垃圾收集和循环长时间占用资源导致程序暂停。
基于信号的抢占式调度器 - Go 1.14 至今
改进:实现了基于信号的真抢占式调度。
缺陷 1:垃圾收集在扫描栈时会触发抢占式调度。
缺陷 2:抢占的时间点不够多,不能覆盖所有边缘情况。
(注:该段文字来源于 抢占式调度器)
协作式抢占是通过在函数调用时插入 抢占检查 来实现抢占的,这种抢占的问题在于,如果 goroutine 中没有函数调用,那就没有办法插入 抢占检查,导致无法抢占。我们看 Go runtime 调度器精讲(七):案例分析 的示例:
//go:nosplit
func gpm() {
var x int
for {
x++
}
}
func main() {
var x int
threads := runtime.GOMAXPROCS(0)
for i := 0; i < threads; i++ {
go gpm()
}
time.Sleep(1 * time.Second)
fmt.Println("x = ", x)
}
禁用异步抢占:
# GODEBUG=asyncpreemptoff=1 go run main.go
程序会卡死。这是因为在 gpm 前插入 //go:nosplit 会禁止函数栈扩张,协作式抢占不能在函数栈调用前插入 抢占检查,导致这个 goroutine 没办法被抢占。
而基于信号的真抢占式调度可以改善这个问题。
1. 基于信号的真抢占式调度
这里我们说的异步抢占指的就是基于信号的真抢占式调度。
异步抢占的实现在 :
func preemptone(pp *p) bool {
...
// Request an async preemption of this P.
if preemptMSupported && debug.asyncpreemptoff == 0 {
pp.preempt = true
preemptM(mp) // 异步抢占
}
return true
}
进入 preemptM:
func preemptM(mp *m) {
...
if mp.signalPending.CompareAndSwap(0, 1) { // 更新 signalPending
signalM(mp, sigPreempt) // signalM 给线程发信号
}
...
}
// signalM sends a signal to mp.
func signalM(mp *m, sig int) {
tgkill(getpid(), int(mp.procid), sig)
}
func tgkill(tgid, tid, sig int)
调用 signalM 给线程发 sigPreempt(_SIGURG:23)信号。线程接收到该信号会做相应的处理。
