这里几乎没有任何承诺。
Go 1.14 release notes 在Runtime section 中这样说:
Goroutines 现在是异步可抢占的。因此,没有函数调用的循环不再可能使调度程序死锁或显着延迟垃圾收集。除windows/arm、darwin/arm、js/wasm 和plan9/* 外,所有平台均支持此功能。
实施抢占的结果是,在 Unix 系统上,包括 Linux 和 macOS 系统,使用 Go 1.14 构建的程序将接收到比使用早期版本构建的程序更多的信号。这意味着使用 syscall 或 golang.org/x/sys/unix 之类的包的程序将看到更慢的系统调用失败并出现 EINTR 错误。 ...
我在这里引用了第三段的一部分,因为这为我们提供了一个关于这种异步抢占如何工作的重要线索:运行时系统让操作系统按照某种时间表传递一些操作系统信号(SIGALRM、SIGVTALRM 等)(真实时间或虚拟时间)。这允许 Go 运行时实现与真实操作系统使用真实(硬件)或虚拟(虚拟化硬件)计时器实现的调度程序相同的类型。与 OS 调度程序一样,由运行时决定如何处理时钟滴答:例如,也许只是运行 GC 代码。
我们还看到了不这样做的平台列表。所以我们可能根本不应该假设它会发生。
幸运的是,运行时源实际上是可用的:如果任何给定的平台实现它,我们可以去看看会发生什么。这表明在runtime/signal_unix.go:
// We use SIGURG because it meets all of these criteria, is extremely
// unlikely to be used by an application for its "real" meaning (both
// because out-of-band data is basically unused and because SIGURG
// doesn't report which socket has the condition, making it pretty
// useless), and even if it is, the application has to be ready for
// spurious SIGURG. SIGIO wouldn't be a bad choice either, but is more
// likely to be used for real.
const sigPreempt = _SIGURG
和:
// doSigPreempt handles a preemption signal on gp.
func doSigPreempt(gp *g, ctxt *sigctxt) {
// Check if this G wants to be preempted and is safe to
// preempt.
if wantAsyncPreempt(gp) && isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()) {
// Inject a call to asyncPreempt.
ctxt.pushCall(funcPC(asyncPreempt))
}
// Acknowledge the preemption.
atomic.Xadd(&gp.m.preemptGen, 1)
atomic.Store(&gp.m.signalPending, 0)
}
实际的asyncPreempt 函数在汇编中,但它只是做了一些纯汇编技巧来保存用户寄存器,然后调用asyncPreempt2 中的runtime/preempt.go:
//go:nosplit
func asyncPreempt2() {
gp := getg()
gp.asyncSafePoint = true
if gp.preemptStop {
mcall(preemptPark)
} else {
mcall(gopreempt_m)
}
gp.asyncSafePoint = false
}
将此与runtime/proc.go 的Gosched 函数进行比较(记录为自愿让步的方式):
//go:nosplit
// Gosched yields the processor, allowing other goroutines to run. It does not
// suspend the current goroutine, so execution resumes automatically.
func Gosched() {
checkTimeouts()
mcall(gosched_m)
}
我们看到主要区别包括一些“异步安全点”的东西,并且我们安排了对 gopreempt_m 而不是 gosched_m 的 M-stack-call。因此,除了安全检查内容和不同的跟踪调用(此处未显示)之外,非自愿抢占与自愿抢占几乎完全相同。
为了找到这一点,我们必须深入挖掘(在本例中为 Go 1.14)实现。人们可能不想过分依赖这一点。