如何理解goroutine的这种行为?

时间:2017-10-22 12:51:23

标签: go goroutine

package main
import (  
    "fmt"
    "time"
)
type field struct {  
    name string
}
func (p *field) print() {  
    fmt.Println(p.name)
}
func main() {  
    data := []field{ {"one"},{"two"},{"three"} }
    for _,v := range data {
        go v.print()
    }
    <-time.After(1 * time.Second)
}

为什么这段代码以任何顺序打印3“三”而不是“一”“两”“三”?

2 个答案:

答案 0 :(得分:4)

有数据竞争。

evaluating arguments to the goroutine function时,代码隐含地获取变量v的地址。请注意,来电v.print()shorthand for the call (&v).print()

循环更改变量v的值。

当goroutines执行时,v具有循环的最后一个值。这不能保证。它可以按照您的预期执行。

使用race detector运行程序非常有用且简单。检测器检测并报告此数据竞争。

一个解决方法是创建另一个作用于循环内部的变量:

for _, v := range data {
    v := v        // short variable declaration of new variable `v`.
    go v.print()
}

通过此更改,在评估goroutine的参数时会获取内部变量v的地址。循环的每次迭代都有一个唯一的内部变量v

另一种解决问题的方法是使用指针:

data := []*field{ {"one"},{"two"},{"three"} } // note '*'
for _, v := range data {
    go v.print()
}

通过此更改,切片中的各个指针将传递给goroutine,而不是范围变量v的地址。

另一个解决方法是使用切片元素的地址:

data := []field{ {"one"},{"two"},{"three"} } // note '*'
for i:= range data {
    v := &data[i]
    go v.print()
}

因为指针值通常与具有指针接收器的类型一起使用,所以这个微妙的问题在实践中并不经常出现。由于field有一个指针接收器,因此对于问题中的[]*field类型,通常使用[]field而不是data

如果goroutine函数在匿名函数中,那么避免该问题的常用方法是将范围变量作为参数传递给匿名函数:

for _, v := range data {
  go func(v field) {
    v.print() // take address of argument v, not range variable v.
  }(v)
}

因为问题中的代码尚未使用goroutine的匿名函数,所以此答案中使用的第一种方法更简单。

答案 1 :(得分:1)

如上所述,存在竞争条件,其结果取决于不同过程的延迟,并且没有明确定义和可预测。 例如,如果添加time.Sleep(1*time.Seconds),您可能会得到正确的结果。因为通常goroutine打印速度超过1秒并且具有正确的变量v,但这是一种非常糟糕的方式。

Golang有一个特殊的竞赛检测工具,有助于找到这种情况。我建议在阅读testing时阅读相关内容。绝对是值得的。

还有另一种方法 - 在goroutine start显式传递变量值:

for _, v := range data {
     go func(iv field) {
            iv.print()
      }(v)
 }

此处v将在每次迭代时复制到iv(“内部v”),每个goroutine将使用正确的值。