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

huangapple go评论70阅读模式
英文:

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

问题

在https://stackoverflow.com/questions/21102078/golang-methods-that-will-yield-goroutines上接受的答案解释了当遇到系统调用时,Go的调度器将从一个goroutine转移到另一个goroutine。我理解这意味着如果你有多个正在运行的goroutine,并且其中一个开始等待像HTTP响应这样的东西,调度器可以将这个作为提示,从该goroutine转移到另一个goroutine。

但是在没有涉及系统调用的情况下呢?例如,假设你有与可用的逻辑CPU核心/线程一样多的goroutine正在运行,并且每个goroutine都在进行不涉及系统调用的CPU密集型计算。理论上,这将使CPU的工作能力饱和。Go调度器是否仍能够检测到从其中一个goroutine转移到另一个goroutine的机会,而这个机会可能不需要那么长的时间来运行,并且然后将控制权返回给执行长时间CPU密集型计算的goroutine之一?

英文:

The accepted answer at https://stackoverflow.com/questions/21102078/golang-methods-that-will-yield-goroutines explains that Go's scheduler will yield control from one goroutine to another when a syscall is encountered. I understand that this means if you have multiple goroutines running, and one begins to wait for something like an HTTP response, the scheduler can use this as a hint to yield control from that goroutine to another.

But what about situations where there are no syscalls involved? What if, for example, you had as many goroutines running as logical CPU cores/threads available, and each were in the middle of a CPU-intensive calculation that involved no syscalls. In theory, this would saturate the CPU's ability to do work. Would the Go scheduler still be able to detect an opportunity to yield control from one of these goroutines to another, that perhaps wouldn't take as long to run, and then return control back to one of these goroutines performing the long CPU-intensive calculation?

答案1

得分: 2

这里没有什么具体的承诺。

Go 1.14发布说明中的Runtime部分中提到:

> Goroutines现在是异步可抢占的。因此,没有函数调用的循环不再可能导致调度器死锁或显著延迟垃圾回收。这在除了windows/armdarwin/armjs/wasmplan9/*之外的所有平台上都得到支持。
>
> 抢占的实现结果是,在包括Linux和macOS在内的Unix系统上,使用Go 1.14构建的程序将比使用早期版本构建的程序接收到更多的信号。这意味着使用syscallgolang.org/x/sys/unix等包的程序将看到更多的慢系统调用失败,并出现EINTR错误。...

我引用了第三段的一部分,因为这给了我们一个关于异步抢占如何工作的重要线索:运行时系统通过某种时间表(真实时间或虚拟时间)让操作系统传递一些操作系统信号(SIGALRM、SIGVTALRM等)。这使得Go运行时能够实现与真实操作系统或虚拟化硬件计时器实现的相同类型的调度器。与操作系统调度器一样,运行时决定如何处理时钟滴答:例如,只运行垃圾回收代码。

我们还可以看到一份不支持此功能的平台列表。因此,我们可能不应该假设它会在所有平台上都发生。

幸运的是,运行时源代码实际上是可用的:我们可以查看任何给定平台上实现的内容。在runtime/signal_unix.go中可以看到:

// 我们使用SIGURG是因为它满足所有这些条件,极不可能被应用程序用于其“真实”含义(因为带外数据基本上未使用,而且SIGURG不报告哪个套接字具有该条件,使其相当无用),即使是这样,应用程序也必须准备好处理虚假的SIGURG。SIGIO也不是一个坏选择,但更有可能被用于真实目的。
const sigPreempt = _SIGURG

以及:

// doSigPreempt在gp上处理抢占信号。
func doSigPreempt(gp *g, ctxt *sigctxt) {
        // 检查此G是否希望被抢占并且是否可以安全抢占。
        if wantAsyncPreempt(gp) && isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()) {
                // 注入对asyncPreempt的调用。
                ctxt.pushCall(funcPC(asyncPreempt))
        }

        // 确认抢占。
        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让出处理器,允许其他goroutine运行。它不挂起当前的goroutine,因此执行会自动恢复。
func Gosched() {
        checkTimeouts()
        mcall(gosched_m)
}

我们可以看到主要的区别包括一些“异步安全点”内容,并且我们安排了一个M堆栈调用gopreempt_m而不是gosched_m。因此,除了安全检查和不显示的不同跟踪调用(此处未显示),非自愿抢占与自愿抢占几乎完全相同。

要找到这些信息,我们必须深入挖掘(在这种情况下是Go 1.14)的实现。可能不应过于依赖这一点。

英文:

There are few if any promises here.

The 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. ...

I quoted part of the third paragraph here because this gives us a big clue as to how this asynchronous preemption works: the runtime system has the OS deliver some OS signal (SIGALRM, SIGVTALRM, etc.) on some sort of schedule (real or virtual time). This allows the Go runtime to implement the same kind of schedulers that real OSes implement with real (hardware) or virtual (virtualized hardware) timers. As with OS schedulers, it's up to the runtime to decide what to do with the clock ticks: perhaps just run the GC code, for instance.

We also see a list of platforms that don't do it. So we probably should not assume it will happen at all.

Fortunately, the runtime source is actually available: we can go look to see what does happen, should any given platform implement it. This shows that in 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

and:

// 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)
}

The actual asyncPreempt function is in assembly, but it just does some assembly-only trickery to save user registers, and then calls asyncPreempt2 which is in runtime/preempt.go:

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

Compare this to runtime/proc.go's Gosched function (documented as the way to voluntarily yield):

//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)
}

We see the main differences include some "async safe point" stuff and that we arrange for an M-stack-call to gopreempt_m instead of gosched_m. So, apart from the safety check stuff and a different trace call (not shown here) the involuntary preemption is almost exactly the same as voluntary preemption.

To find this, we had to dig rather deep into the (Go 1.14, in this case) implementation. One might not want to depend too much on this.

答案2

得分: 0

关于这个问题,我来给你翻译一下:

稍微补充一下 @torek 的回答。当存在系统调用、协程等待锁、通道或休眠时,协程是可中断的。

正如 @torek 所说,自从 1.14 版本以后,即使协程没有执行上述操作,它们也可以被抢占。调度器在协程运行超过10毫秒后,可以将任何协程标记为可抢占的。

更多相关信息请参考:https://medium.com/a-journey-with-go/go-goroutine-and-preemption-d6bc2aa2f4b7

英文:

A little bit more on this to complete @torek's answer.
Goroutines are interruptible when there is a syscall, but also when a routine is waiting on a lock, a chan or sleeping.

As @torek's said, since 1.14 routines can also be preempted even when they do none of the above. The scheduler can mark any routine as preemptible after it ran for more than 10ms.

More reading there: https://medium.com/a-journey-with-go/go-goroutine-and-preemption-d6bc2aa2f4b7

huangapple
  • 本文由 发表于 2021年8月8日 08:00:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/68696886.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定