简体   繁体   English

goroutine 可以在什么时候产生?

[英]At which point a goroutine can yield?

I am trying to gain better understanding of how goroutines are scheduled in Go programs, especially at which points they can yield to other goroutines.我试图更好地了解 Go 程序中 goroutines 是如何调度的,尤其是在它们可以让步给其他 goroutines 的时候。 We know that a goroutine yields on syscals that would block it, but apparently this is not the whole picture.我们知道 goroutine 会产生会阻塞它的 syscals,但显然这不是全部。

This question raises somewhat of similar concern, and the most rated answers says that a goroutine may also switch on function calls, as doing that would call the scheduler to check if the stacks needs to be grown, but it explicitly says that 这个问题引起了一些类似的关注,最受好评的答案是 goroutine 也可以打开函数调用,因为这样做会调用调度程序来检查堆栈是否需要增长,但它明确表示

If you don't have any function calls, just some math, then yes, goroutine will lock the thread until it exits or hits something that could yield execution to others.如果你没有任何函数调用,只是一些数学运算,那么是的,goroutine 将锁定线程,直到它退出或遇到可能让其他人执行的东西。

I wrote a simple program to check and prove that:我写了一个简单的程序来检查和证明:

package main

import "fmt"

var output [30]string      // 3 times, 10 iterations each.
var oi = 0

func main() {
    runtime.GOMAXPROCS(1)   // Or set it through env var GOMAXPROCS.
    chanFinished1 := make(chan bool)
    chanFinished2 := make(chan bool)

    go loop("Goroutine 1", chanFinished1)
    go loop("Goroutine 2", chanFinished2)
    loop("Main", nil)

    <- chanFinished1
    <- chanFinished2

    for _, l := range output {
        fmt.Println(l)
    }
}

func loop(name string, finished chan bool) {
    for i := 0; i < 1000000000; i++ {
        if i % 100000000 == 0 {
            output[oi] = name
            oi++
        }
    }

    if finished != nil {
        finished <- true
    }
}

NOTE: I am aware that putting a value in the array and incrementing oi without synchronization is not quite correct, but I want to keep the code easy and free of things that could cause switching.注意:我知道在数组中放置一个值并在不同步的情况下增加oi不太正确,但我希望保持代码简单并且没有可能导致切换的东西。 After all, the worst thing that can happen is putting a value without advancing the index (overwriting), which is not a big deal.毕竟,可能发生的最糟糕的事情是在不推进索引(覆盖)的情况下放置一个值,这没什么大不了的。

Unlike this answer , I avoided using of any function calls (including built-in append() ) from the loop() function that is launched as a goroutine, also I am explicitly setting GOMAXPROCS=1 which according to documentation :此答案不同,我避免使用作为 goroutine 启动的loop()函数中的任何函数调用(包括内置的append() ),而且我根据文档明确设置了GOMAXPROCS=1

limits the number of operating system threads that can execute user-level Go code simultaneously.限制可以同时执行用户级 Go 代码的操作系统线程数。

Nevertheless, in the output I still see the messages Main / Goroutine 1 / Goroutine 2 interleaved, meaning one of following:尽管如此,在输出中我仍然看到消息Main / Goroutine 1 / Goroutine 2交错,意思是以下之一:

  • execution of the goroutine interrupts and the goroutine gives up the control at some moments; goroutine 的执行中断,goroutine 在某些时刻放弃控制;
  • GOMAXPROCS does not work as stated in the documentation, spinning up more OS threads to schedule goroutines. GOMAXPROCS不像文档中所说的那样工作,它会启动更多的操作系统线程来调度 goroutine。

Either the answer is not complete, or some things have changed since 2016 (I tested on Go 1.13.5 and 1.15.2).要么答案不完整,要么自 2016 年以来发生了一些变化(我在 Go 1.13.5 和 1.15.2 上进行了测试)。

I am sorry if the question was answered, but I failed to find neither explanation of why this particular example yields control, nor about points where goroutines yield control in general (excepting blocking syscalls).如果问题得到了回答,我很抱歉,但我既没有找到关于为什么这个特定示例产生控制的解释,也没有找到 goroutine 通常产生控制的点(阻塞系统调用除外)。

NOTE: This question is purely theoretical, I am not trying to solve any practical task now, but in general, I assume that knowing points where a goroutine can yield and where it cannot allows us to avoid redundant usage of synchronization primitives.注意:这个问题纯粹是理论性的,我现在不打算解决任何实际任务,但总的来说,我假设知道 goroutine 可以让步和不能让我们避免冗余使用同步原语的点。

Go version 1.14 introduced asynchronous preemption: Go 1.14 版引入了异步抢占:

Goroutines are now asynchronously preemptible. Goroutines 现在是异步可抢占的。 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/* .这在除windows/armdarwin/armjs/wasmplan9/*之外的所有平台上都受支持。

As answered in Are channel sends preemption points for goroutine scheduling?通道是否为 goroutine 调度发送抢占点中的回答 , Go's preemption points may change from one release to the next. ,Go 的抢占点可能会从一个版本到下一个版本发生变化。 Asynchronous preemption just adds possible preemption points almost everywhere.异步抢占只是在几乎所有地方都增加了可能的抢占点。

Your writes to the output array are not synchronized and your oi index is not atomic, which means we can't really be sure what happens in terms of the output array.您对output数组的写入不同步,并且您的oi索引不是原子的,这意味着我们无法确定输出数组会发生什么。 Of course, adding atomicity to it with a mutex introduces cooperative scheduling points.当然,使用互斥体为其添加原子性会引入协作调度点。 While these aren't the source of cooperative scheduling switches (which must be occurring based on your output), they do mess with our understanding of the program.虽然这些不是协作调度切换的来源(必须根据您的输出发生),但它们确实干扰了我们对程序的理解。

The output array holds strings, and using strings can invoke the garbage collection system, which can use locks and cause scheduling switching. output数组保存字符串,使用字符串可以调用垃圾收集系统,垃圾收集系统可以使用锁并导致调度切换。 So this is the most likely cause of scheduling switching in pre-Go-1.14 implementations.所以这是在 Go-1.14 之前的实现中调度切换的最可能原因。

As @torek has pointed out the most popular runtime environments for GO have used pre-emptive scheduling for a few months now (since 1.14).正如@torek 所指出的,最流行的 GO 运行时环境已经使用抢占式调度了几个月(自 1.14 以来)。 Otherwise the points at which a goroutine may yield varies depending on the runtime environment and the release but William Kennedy gives a good summary.否则,goroutine 可能产生的点会因运行时环境和版本而异,但William Kennedy给出了很好的总结。

I also recall that there was an option added to the compiler to add yield points to long running loops a few years ago, but this was an experimental option not normally triggered.我还记得几年前在编译器中添加了一个选项,可以为长时间运行的循环添加屈服点,但这是一个通常不会触发的实验性选项。 (Of course you can do it manually by calling runtime.GoSched every now and then on your loop.) (当然,您可以通过在循环中runtime.GoSched调用runtime.GoSched来手动执行此操作。)

As for your test I am surprised by the result you got when running under Go 1.13.5.至于你的测试,我对你在 Go 1.13.5 下运行时得到的结果感到惊讶。 The behaviour is not exactly defined due to the data races (I know you avoided any synchronisation mechanisms to avoid triggering a yield) but I would not have expected that result.由于数据竞争(我知道您避免了任何同步机制以避免触发产量),行为并未完全定义,但我没想到会出现这种结果。 One thing is that setting GOMAXPROCS to 1 will mean that only one goroutine is executing concurrently but that may not necessarily mean when a different goroutine executes it will run on the same core.一件事是将GOMAXPROCS设置为 1 将意味着只有一个 goroutine 在并发执行,但这可能并不一定意味着当不同的 goroutine 执行时它会在同一个核心上运行。 A different core will have a different cache and (without synchronisation) different opinions of the values of output and oi .不同的核心将具有不同的缓存,并且(没有同步)对outputoi的值有不同的看法。

But may I suggest you simply forget about modifying global variables and just log a message before and after the busy-loop.但是我可以建议您简单地忘记修改全局变量,只需在繁忙循环之前和之后记录一条消息。 This should clearly show (in GO < 1.14) that only one lopp will run at a time.这应该清楚地表明(在 GO < 1.14 中)一次只会运行一个 lopp。 (I was trying do the same experiment as you many years ago and that seemed to work.) (多年前我曾尝试和你做同样的实验,这似乎奏效了。)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM