Golang如何在goroutine之间共享变量?

19

我正在学习Go语言并尝试理解其并发功能。

我有以下程序。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)

        x := i

        go func() {
            defer wg.Done()
            fmt.Println(x)
        }()

    }

    wg.Wait()
    fmt.Println("Done")
}

执行时我得到:

4
0
1
3
2

这正是我想要的,不过如果我对它做些微小的修改:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)

        go func() {
            defer wg.Done()
            fmt.Println(i)
        }()

    }

    wg.Wait()
    fmt.Println("Done")
}

我得到的将会是:

5
5
5
5
5

我不太理解它们之间的区别。有人可以帮忙解释一下这里发生了什么,以及Go运行时如何执行这段代码吗?


5
查看常见问题解答:https://golang.org/doc/faq#closures_and_goroutines - JimB
2
另请参阅Go语言内存模型 - icza
1
可能存在重复问题:https://dev59.com/boDba4cB1Zd3GeqPGJnQ,https://dev59.com/lV8e5IYBdhLWcg3wRIpQ - JimB
请参见交互式的Go陷阱 - Plato
3个回答

15

每次运行 x := i 时,您都会有一个新的变量,
通过在 goroutine 中打印 x 的地址,此代码可以很好地显示差异:
The Go Playground

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        x := i
        go func() {
            defer wg.Done()
            fmt.Println(&x)
        }()
    }
    wg.Wait()
    fmt.Println("Done")
}

输出:

0xc0420301e0
0xc042030200
0xc0420301e8
0xc0420301f0
0xc0420301f8
Done

使用go build -race构建您的第二个示例并运行它:
您将看到:警告:数据竞争


这将是很好的Go Playground:

//go build -race
package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            fmt.Println(i)
        }(i)
    }
    wg.Wait()
    fmt.Println("Done")
}

输出:

0
4
1
2
3
Done

1
谢谢。这帮助我很多地理解了背后的逻辑。 - user130268
最后一个例子是否正确,因为在协程函数启动/调用时变量i被复制了? - xuiqzy
@xuiqzy 给一个新的 goroutine 传递一个值,而不是在 goroutine 中引用它。 - hao

7

一般规则是,不要在goroutine之间共享数据。在第一个例子中,你实际上给每个goroutine分配了自己的x副本,并且它们按照任意顺序打印出来。在第二个例子中,它们都引用相同的循环变量,并且在它们任何一个打印它之前,它被递增到5。我不认为那里的输出是有保证的,只是恰好创建goroutines的循环完成得比它们自己到达打印部分的时间更快。


2

用简单易懂的语言解释有点困难,但我会尽力。

你看,每次生成一个新的 goroutine 时,都需要初始化时间,无论这个时间多小,它总是存在的。因此,在第二种情况下,在任何 goroutine 开始之前,整个循环已经完成了 5 次增加变量的操作。当 goroutine 完成初始化后,它们所看到的只是最终变量值为 5。

然而,在第一种情况下,x 变量保留了 i 变量的副本,因此当 goroutine 启动时,x 被传递给它们。请记住,在这里增加的是 i,而不是 x。x 是固定的。因此,当 goroutine 启动时,它们得到的是一个固定的值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接