Go 的调度程序是否会将对 CPU 密集型工作的控制权从一个 goroutine 转移到另一个 goroutine?

Will Go's scheduler yield control from one goroutine to another for CPU-intensive work?

golang methods that will yield goroutines 接受的答案解释说,当遇到系统调用时,Go 的调度程序会将控制权从一个 goroutine 交给另一个 goroutine。我知道这意味着如果你有多个 goroutines 运行ning,并且其中一个开始等待 HTTP 响应之类的东西,调度程序可以使用它作为提示将控制权从那个 goroutine 交给另一个 goroutine。

但是不涉及系统调用的情况呢?例如,如果您有与逻辑 CPU cores/threads 一样多的 goroutines 运行ning 可用,并且每个 goroutines 都处于 CPU 密集计算的中间,不涉及系统调用。理论上,这会使 CPU 的工作能力饱和。 Go 调度程序是否仍然能够检测到将这些 goroutine 之一的控制权交给另一个 goroutine 的机会,这可能不会花那么长时间 运行,然后 return 将控制权交还给其中一个goroutines 执行长时间的 CPU 密集计算?

这里几乎没有任何承诺。

Go 1.14 release notes says this in the Runtime section:

Goroutines are now asynchronously preemptible. As a result, loops without function calls no longer potentially deadlock the scheduler or significantly delay garbage collection. This is supported on all platforms except windows/arm, darwin/arm, js/wasm, and plan9/*.

A consequence of the implementation of preemption is that on Unix systems, including Linux and macOS systems, programs built with Go 1.14 will receive more signals than programs built with earlier releases. This means that programs that use packages like syscall or golang.org/x/sys/unix will see more slow system calls fail with EINTR errors. ...

我在这里引用了第三段的一部分,因为这给了我们一个关于异步抢占如何工作的重要线索:运行时间系统让 OS 提供了一些 OS信号(SIGALRM、SIGVTALRM 等)按某种时间表(实时或虚拟时间)。这允许 Go 运行time 实现与真实 OSes 使用真实(硬件)或虚拟(虚拟化硬件)计时器实现的相同类型的调度程序。与 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 函数在汇编中,但它只是做了一些仅用于汇编的技巧来保存用户寄存器,然后调用 runtime/preempt.go 中的 asyncPreempt2:

//go:nosplit
func asyncPreempt2() {
        gp := getg()
        gp.asyncSafePoint = true
        if gp.preemptStop {
                mcall(preemptPark)
        } else {
                mcall(gopreempt_m)
        }
        gp.asyncSafePoint = false
}

将此与 runtime/proc.goGosched 功能进行比较(记录为自愿屈服的方式):

//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 堆栈调用。因此,除了安全检查内容和不同的跟踪调用(此处未显示)之外,非自愿抢占几乎与自愿抢占完全相同。

为了找到这个,我们不得不深入挖掘(在本例中为 Go 1.14)实现。人们可能不想过多地依赖于此。

更多关于此的内容以完成@torek 的回答。 当有系统调用时,Goroutines 是可中断的,但当例程正在等待锁、chan 或睡眠时也是如此。

正如@torek 所说,因为 1.14 例程即使在执行上述 none 时也可以被抢占。调度程序可以在 运行 超过 10 毫秒后将任何例程标记为可抢占。

更多阅读:https://medium.com/a-journey-with-go/go-goroutine-and-preemption-d6bc2aa2f4b7