【问题标题】:Will Go's scheduler yield control from one goroutine to another for CPU-intensive work?Go 的调度程序会为 CPU 密集型工作从一个 goroutine 控制到另一个 goroutine 吗?
【发布时间】:2021-08-08 00:00:28
【问题描述】:

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

但是不涉及系统调用的情况呢?例如,如果您运行的 goroutine 与可用的逻辑 CPU 内核/线程一样多,并且每个 goroutine 都处于不涉及系统调用的 CPU 密集型计算的中间,该怎么办。理论上,这会使 CPU 的工作能力饱和。 Go 调度程序是否仍然能够检测到将控制权从这些 goroutine 之一让给另一个的机会,这可能不会花费很长时间来运行,然后将控制权返回给这些 goroutine 中的一个执行长时间的 CPU 密集型计算?

【问题讨论】:

  • 更新的版本(从 1.14 IIRC 开始)可以抢占 goroutine,这意味着即使 goroutine 没有发出任何系统调用,它仍然可以产生。
  • 是的,这就是答案。
  • 没有“唯一的Go调度程序”。这东西在进化。
  • "即使没有发出任何系统调用,仍然可以让步"这里不要太迂腐,但我很好奇,他们是否可以让步(并且程序员必须做一些特别的事情来实现)或者他们自动屈服?

标签: multithreading go concurrency scheduler goroutine


【解决方案1】:

这里几乎没有任何承诺。

Go 1.14 release notesRuntime section 中这样说:

Goroutines 现在是异步可抢占的。因此,没有函数调用的循环不再可能使调度程序死锁或显着延迟垃圾收集。除windows/armdarwin/armjs/wasmplan9/* 外,所有平台均支持此功能。

实施抢占的结果是,在 Unix 系统上,包括 Linux 和 macOS 系统,使用 Go 1.14 构建的程序将接收到比使用早期版本构建的程序更多的信号。这意味着使用 syscallgolang.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.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-stack-call。因此,除了安全检查内容和不同的跟踪调用(此处未显示)之外,非自愿抢占与自愿抢占几乎完全相同。

为了找到这一点,我们必须深入挖掘(在本例中为 Go 1.14)实现。人们可能不想过分依赖这一点。

【讨论】:

  • 很好,感谢您挖掘所有这些信息。在我看来,这绝对不是一个人想要依赖的东西。他们应该编写他们的函数,期望 Go 运行时会阻塞他们,直到函数完成,除非他们愿意使用 runtime.Gosched 来拆分他们的工作。
【解决方案2】:

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-01-10
    • 2013-08-06
    • 2021-08-15
    • 1970-01-01
    • 2017-08-06
    • 2016-09-24
    • 2013-07-28
    相关资源
    最近更新 更多