英文:
Why is there a race condition in this program?
问题
我正在查看Golang文档中的典型数据竞争,我不太明白为什么这个程序会有问题:
func main() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // 不是你要找的 'i'。
wg.Done()
}()
}
wg.Wait()
}
当我期望它打印出 0, 1, 2, 3, 4
(不一定按照这个顺序)时,它却打印出 5, 5, 5, 5, 5
。
在我看来,当循环内创建goroutine时,i
的值是已知的(例如,可以在循环开始时执行 log.Println(i)
来查看预期值)。所以我期望goroutine在创建时捕获i
的值,并在后面使用。
显然这不是发生的事情,但为什么呢?
英文:
I'm looking at the typical data races in the Golang documentation, and I don't quite understand why there is a problem with this program:
func main() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // Not the 'i' you are looking for.
wg.Done()
}()
}
wg.Wait()
}
It prints 5, 5, 5, 5, 5
when I would expect it to print 0, 1, 2, 3, 4
(not necessarily in this order).
The way I see it, when the goroutine gets created inside the loop, the value of i
is known (for instance, one could do a log.Println(i)
at the beginning of the loop and see the expected value). So I would expect the goroutine to capture the value of i
when it gets created and use that later on.
Obviously it's not what's happening but why?
答案1
得分: 8
你的函数字面量引用了外部作用域中的 i
。如果你请求 i
的值,你会得到当前 i
的值。为了使用创建 Go 协程时的 i
的值,提供一个参数:
func main() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(i int) {
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
}
英文:
Your function literal references the i
from the outer scope. If you request the value of i
, you get the value of whatever i
is right now. In order to use the value of i
at the time the Go routine was created, supply an argument:
func main() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(i int) {
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
}
答案2
得分: 2
变量i
在函数字面量中没有声明,所以它成为闭包的一部分。理解闭包的一种简单方法是思考它们如何实现。简单的解决方案是使用指针。你可以认为编译器将函数字面量重写为以下形式:
func f123(i *int) {
fmt.Println(*i)
wg.Done
}
-
在调用该函数时,通过go语句将
i
变量的地址传递给被调用的f123函数(编译器生成的示例名称)。 -
你可能正在使用默认的GOMAXPROCS==1,所以for循环在没有任何调度的情况下执行5次,因为循环没有I/O或其他“调度点”,比如通道操作。
-
当循环终止时,
i == 5
,wg.Wait
最终触发执行五个准备运行的goroutine(对于f123)。它们当然都有指向相同整数变量i
的指针。 -
每个goroutine现在都看到相同的
i
值为5。
当使用GOMAXPROCS > 1运行时,或者循环让出控制时,你可能会得到不同的输出。这也可以通过例如runtime.Gosched来实现。
英文:
The variable i
is not declared within the function literal, so it becomes part of a closure. An easy way how to understand closures is to think about how can they be implemented. The simple solution is using a pointer. You can think that the function literal is rewritten by the compiler into some
func f123(i *int) {
fmt.Println(*i)
wg.Done
}
-
On invocation of this function, by the go statement, the address of the
i
variable is passed to the called f123 (example name generated by the compiler). -
You're probably using default GOMAXPROCS==1, so the for loop executes 5 times without any scheduling as the loop does no I/O or other "schedule points", such as channel operations.
-
When the loop terminates, with
i == 5
, thewg.Wait
finally triggers execution of the five, ready to run, goroutines (for f123). All of them have of course the same pointer to the same integer variablei
. -
Every goroutine now sees the same
i
value of 5.
You might get different output when running with GOMAXPROCS > 1, or when the loop yields control. That can be done also by, for example, runtime.Gosched.
答案3
得分: 0
正如其他人所提到的,你的变量i
在你创建的goroutine中使用,但是这些goroutine可能在将来的某个时候执行,一旦你的循环已经完成。此时,变量i
的值不是5
,而且所有的goroutine都会启动,读取变量i
的值(作为5
)并继续它们的工作。
我相信FUZxxl提到了将值i
作为参数传递给函数的用法。我认为这对于相当复杂的系统是一个好主意,特别是如果你为go routine启动的函数不是内联闭包。然而,在大多数情况下,我认为为每个go routine创建一个新的临时变量更加清晰:
http://play.golang.org/p/6dnkrEGfhn
func main() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
myi := i
go func() {
fmt.Println(myi)
wg.Done()
}()
}
wg.Wait()
}
效果是一样的,可以说这是个人偏好,而且确实是。这是我的偏好 :p
英文:
As mentioned by others, your variable i
is used inside the goroutines that you're created, but those goroutines could execute way in the future, once your loop is already done looping. At this point, the value of i
is not 5
, and all your go routines get kicked up, read the value of i
(as 5
) and continue on their merry way.
I believe FUZxxl mentioned the use of passing the value i
as an argument to the function. I think this is a good idea for rather complicated systems, specially if the function you're kicking a go routine for is not an inline closure. However, in most cases, I think it's a lot cleaner to just create a new temporary variable for each go routine:
http://play.golang.org/p/6dnkrEGfhn
<!-- language-all: golang -->
func main() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
myi := i
go func() {
fmt.Println(myi)
wg.Done()
}()
}
wg.Wait()
}
The effect is the same, and it could be argued that it's a matter of preference, and it is. This is my preference :p
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论