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)信号。线程接收到该信号会做相应的处理。
阅读全文