【问题标题】:Why do goroutines not execute until the for-loop has terminated?为什么 goroutine 直到 for 循环终止才执行?
【发布时间】:2021-11-30 16:05:59
【问题描述】:

和你们大多数人一样,我很熟悉 Go 在 for 循环中重用迭代器变量的事实,因此每个 goroutine 的闭包都会捕获相同的变量。下面的代码就是一个典型的例子,它总是会从每个 goroutine 中打印出循环的最终值:

func main() {
    for i := 0; i < 5; i++ {
        go func() {
            fmt.Println(i) // prints 5 each time
        }()
    }
    time.Sleep(100 * time.Millisecond)
}

我无法找到解释的是,为什么 goroutine 直到循环完成后才会执行。甚至下面的代码也会将ii 的值生成为10,这是在调用goroutine 之后设置的:

func main() {
    for i := 0; i < 5; i++ {
        ii := i
        go func() {
            fmt.Println(ii) // prints 10 each time...!
        }()
        ii = 10
    }
    time.Sleep(100 * time.Millisecond)
}

与我读过的解释最接近的是 for 循环通常比 goroutine 执行得更快。这给我提出了两个问题: 1. 这是真的吗? 2. 为什么?

【问题讨论】:

  • "将 ii 的值生成为 10,这是在调用 goroutine 之后设置的" goroutines 没有被“调用”。最好说go f()“创建”一个新的goroutine,一旦goroutine真正开始运行,它将执行f。
  • “我读过的最接近解释的是 for 循环通常比 goroutine 执行得更快。这给我提出了两个问题:1. 这是真的吗?”是的!不!比较 goroutine 和 goroutine 执行的 for 循环的速度是没有意义的。每个代码都由一个 goroutine 执行:你的 for 循环由执行 main 的 goroutine 执行。是的,这也是一个 goroutine(它是在程序启动期间生成的,而不是手动使用 go)。
  • 会发生什么:主 goroutine 正在愉快地处理您的 for 循环。这个循环做了一些变量赋值并产生了一些 goroutines。只是偶然(绝对不能保证 goroutines 是如何调度的)主 goroutine 到达 time.Sleep 在任何新的 goroutines 开始执行它们的 Printf 工作之前。如果没有同步(通道、互斥体、等待组等),任何 goroutine 执行顺序都可能发生。
  • 并且:推理具有竞争条件的程序是错误的。
  • @Volker 感谢您耐心地纠正我的(许多)误解。您提到“偶然” goroutine 到达 time.Sleep 在 goroutine 执行 Println 语句之前。我理解为什么这应该只是一个机会,但是非常可靠地运行此代码会产生相同的结果。我不应该期望看到更多的结果变化吗?例如。 play.golang.org/p/PA4veXPOd6W

标签: go goroutine


【解决方案1】:

一切皆有可能,因为程序存在数据竞争。

撇开数据竞争不谈,没有证据表明 goroutine 在 for 循环完成后执行。值 10 在 goroutine 启动后的语句中分配给 ii,而不是在 for 循环之后。

内存模型允许编译器将两个 ii 赋值优化为在 for 循环体开始处的单个赋值。可能是 goroutine 立即执行,但 goroutine 从优化中看到了 10 的值。

【讨论】:

  • 正如沃尔克指出的那样,我是从错误的假设开始的。您对优化的解释对我来说很有意义,并解释了为什么每次运行此代码都会产生完全相同的结果(尽管原则上它可能会有所不同)。
【解决方案2】:

永远不要在处理多个 goroutine 时假定执行顺序 - 不管它有多诱人。不要睡觉;不要打电话给runtime.Gosched;不要假设任何事情

确保执行顺序的唯一保证方法是通过同步方法,例如通道、sync.Mutexsync.WaitGroups 等。

【讨论】:

    【解决方案3】:

    你应该像这样使用 goroutine:

    package main
    
    import "fmt"
    import "time"
    
    func main() {
        for i := 0; i < 5; i++ {
            ii := i
            go func(k int) {
                fmt.Println(k) // prints 10 each time...!
            }(ii)
            ii = 10
        }
        time.Sleep(100 * time.Millisecond)
    }
    

    【讨论】:

    • 确实可以解决问题。但我的问题是为什么它开始打印 10!
    • @vordhosbn 你可以使用runtime.Gosched 来防止forloop goroutine 先运行:play.golang.org/p/y5w1unj9mpx
    • @lidaobing 小心!来自runtime.Gosched 文档Gosched yields the processor, allowing other goroutines to run. It does not suspend the current goroutine, so execution resumes automatically. - 所以运行这不是保证修复。
    • @colm.anseo 这不是一个修复,而是对 goroutine 如何工作的解释。
    猜你喜欢
    • 2018-10-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-02-08
    • 1970-01-01
    • 2015-06-28
    • 2013-11-23
    相关资源
    最近更新 更多