Golang 基于信号的异步抢占与处理
在Go1.14版本开始实现了 基于信号的协程抢占调度 模式,在此版本以前执行以下代码是永远也无法执行最后一条println语句。
本文基于go version 1.16
package main
import (
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(1)
go func() {
for {
}
}()
time.Sleep(time.Millisecond)
println("OK")
}
原因很简单:在main函数里只有一个CPU,从上到下执行到 time.Sleep() 函数的时候,会将 main goroutine 放入运行队列,出让了P,开始执行匿名函数,但匿名函数是一个for循环,没有任何 IO 语句,也就无法引起对 G 的调度,所以当前仅有的一个 P 永远被其占用,导致无法打印OK。
这个问题在1.14版本开始有所改变,主要是因为引入了基于信号的抢占模式。在程序启动时,初始化信号,并在 runtime.sighandler 函数注册了 SIGURG 信号的处理函数 runtime.doSigPreempt,然后在触发垃圾回收的栈扫描时或执行 sysmon 监控线程时,调用函数挂起goroutine,并向M发送信号,M收到信号后,会让当前goroutine陷入休眠继续执行其他的goroutine。
本篇从发送与接收信号并处理两方面来看一下它是如何实现的。
发送信号
在上篇文章( 认识sysmon监控线程)介绍 sysmon 的时候,我们知道监控线程会在无P的情况下一直运行,定期扫描所有的P,将长时间运行的G 进行解除。
// Always runs without a P, so write barriers are not allowed.
//
//go:nowritebarrierrec
func sysmon() {
......
for {
if idle == 0 { // start with 20us sleep...
delay = 20
} else if idle > 50 { // start doubling the sleep after 1ms...
delay *= 2
}
if delay > 10*1000 { // up to 10ms
delay = 10 * 1000
}
usleep(delay)
......
// retake P's blocked in syscalls
// and preempt long running G's
if retake(now) != 0 {
idle = 0
} else {
idle++
}
}
......
}
通过 retake() 函数对所有 P 进行检查。
By admin
read more
P的状态切换
sysmon
M0 与 g0的区别
segment stack
Contiguous stacks 扩容与收缩
runtime.main
GPM 协作
