为什么这个程序中会出现竞态条件?

huangapple go评论78阅读模式
英文:

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 &lt; 5; i++ {
		go func() {
			fmt.Println(i) // Not the &#39;i&#39; 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 &lt; 5; i++ {
        go func(i int) {
            fmt.Println(i)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

runnable example

答案2

得分: 2

变量i在函数字面量中没有声明,所以它成为闭包的一部分。理解闭包的一种简单方法是思考它们如何实现。简单的解决方案是使用指针。你可以认为编译器将函数字面量重写为以下形式:

func f123(i *int) {
        fmt.Println(*i)
        wg.Done            
}
  • 在调用该函数时,通过go语句将i变量的地址传递给被调用的f123函数(编译器生成的示例名称)。

  • 你可能正在使用默认的GOMAXPROCS==1,所以for循环在没有任何调度的情况下执行5次,因为循环没有I/O或其他“调度点”,比如通道操作。

  • 当循环终止时,i == 5wg.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, the wg.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 variable i.

  • 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 &lt; 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

huangapple
  • 本文由 发表于 2013年6月21日 23:11:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/17238650.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定