Go语言 追加不是线程安全的?

dtcbnfnu  于 2022-12-07  发布在  Go
关注(0)|答案(5)|浏览(106)

我注意到,如果我在for循环中使用goroutine追加到一个切片上,就会出现缺失/空白数据的情况:

destSlice := make([]myClass, 0)

var wg sync.WaitGroup
for _, myObject := range sourceSlice {
    wg.Add(1)
    go func(closureMyObject myClass) {
        defer wg.Done()
        var tmpObj myClass
        tmpObj.AttributeName = closureMyObject.AttributeName
        destSlice = append(destSlice, tmpObj)
    }(myObject)
}
wg.Wait()

有时,当我打印destSlice中的所有AttributeName时,有些元素是空字符串(""),而其他时候,sourceSlice中的一些元素在destSlice中不存在。
我的代码是否存在数据竞争,这是否意味着append对于多个goroutine的并发使用来说不是线程安全的?

yyhrrdl8

yyhrrdl81#

在Go语言中,没有一个值对于并发读写是安全的,片(片头)也不例外。
是,您的程式码有数据竞争。请使用-race选项执行以进行验证。
示例:

type myClass struct {
    AttributeName string
}
sourceSlice := make([]myClass, 100)

destSlice := make([]myClass, 0)

var wg sync.WaitGroup
for _, myObject := range sourceSlice {
    wg.Add(1)
    go func(closureMyObject myClass) {
        defer wg.Done()
        var tmpObj myClass
        tmpObj.AttributeName = closureMyObject.AttributeName
        destSlice = append(destSlice, tmpObj)
    }(myObject)
}
wg.Wait()

运行它与

go run -race play.go

输出为:

==================
WARNING: DATA RACE
Read at 0x00c420074000 by goroutine 6:
  main.main.func1()
      /home/icza/gows/src/play/play.go:20 +0x69

Previous write at 0x00c420074000 by goroutine 5:
  main.main.func1()
      /home/icza/gows/src/play/play.go:20 +0x106

Goroutine 6 (running) created at:
  main.main()
      /home/icza/gows/src/play/play.go:21 +0x1cb

Goroutine 5 (running) created at:
  main.main()
      /home/icza/gows/src/play/play.go:21 +0x1cb
==================
==================
WARNING: DATA RACE
Read at 0x00c42007e000 by goroutine 6:
  runtime.growslice()
      /usr/local/go/src/runtime/slice.go:82 +0x0
  main.main.func1()
      /home/icza/gows/src/play/play.go:20 +0x1a7

Previous write at 0x00c42007e000 by goroutine 5:
  main.main.func1()
      /home/icza/gows/src/play/play.go:20 +0xc4

Goroutine 6 (running) created at:
  main.main()
      /home/icza/gows/src/play/play.go:21 +0x1cb

Goroutine 5 (running) created at:
  main.main()
      /home/icza/gows/src/play/play.go:21 +0x1cb
==================
==================
WARNING: DATA RACE
Write at 0x00c420098120 by goroutine 80:
  main.main.func1()
      /home/icza/gows/src/play/play.go:20 +0xc4

Previous write at 0x00c420098120 by goroutine 70:
  main.main.func1()
      /home/icza/gows/src/play/play.go:20 +0xc4

Goroutine 80 (running) created at:
  main.main()
      /home/icza/gows/src/play/play.go:21 +0x1cb

Goroutine 70 (running) created at:
  main.main()
      /home/icza/gows/src/play/play.go:21 +0x1cb
==================
Found 3 data race(s)
exit status 66

解决方法很简单,用一个sync.Mutex来保护写destSlice的值:

var (
    mu        = &sync.Mutex{}
    destSlice = make([]myClass, 0)
)

var wg sync.WaitGroup
for _, myObject := range sourceSlice {
    wg.Add(1)
    go func(closureMyObject myClass) {
        defer wg.Done()
        var tmpObj myClass
        tmpObj.AttributeName = closureMyObject.AttributeName
        mu.Lock()
        destSlice = append(destSlice, tmpObj)
        mu.Unlock()
    }(myObject)
}
wg.Wait()

你也可以用其他方法来解决这个问题,比如,你可以使用一个通道来发送要追加的值,然后指定一个goroutine从这个通道接收并执行追加。
另请注意,虽然切片头不安全,但切片元素充当不同的变量,并且可以同时写入不同的切片元素而无需同步(因为它们是不同的变量)。请参见我可以同时写入不同的切片元素吗

6za6bjd0

6za6bjd02#

这是一个很老的问题,但是还有一个小的改进,可以帮助摆脱互斥锁。你可以使用索引来添加到数组中。每个go例程都将使用它自己的索引。在这种情况下,同步是不必要的。

destSlice := make([]myClass, len(sourceSlice))

var wg sync.WaitGroup
for i, myObject := range sourceSlice {
    wg.Add(1)
    go func(idx int, closureMyObject myClass) {
        defer wg.Done()
        var tmpObj myClass
        tmpObj.AttributeName = closureMyObject.AttributeName

        destSlice[idx] = tmpObj
     }(i, myObject)
}
wg.Wait()
0md85ypi

0md85ypi3#

为了给这个问题给予一个更新的解决方案,看起来Go发布了一个新的用于同步的Map:
https://godoc.org/golang.org/x/sync/syncmap

mm9b1k5b

mm9b1k5b4#

问题已经得到了回答,但是我最喜欢的解决这个问题的方法是使用errgroup。文档中的一个例子是这个确切的问题加上一个很好的附加错误处理。
下面是文档中示例的内容:

g, ctx := errgroup.WithContext(ctx)

searches := []Search{Web, Image, Video}
results := make([]Result, len(searches))
for i, search := range searches {
    i, search := i, search // https://golang.org/doc/faq#closures_and_goroutines
    g.Go(func() error {
        result, err := search(ctx, query)
        if err == nil {
            results[i] = result
        }
        return err
    })
}
if err := g.Wait(); err != nil {
    return nil, err
}
return results, nil

希望这对那些不知道errgroup包的人有帮助。

wwwo4jvm

wwwo4jvm5#

Go新手请注意--只有@lockwobr的答案会将range语句输出作为新的局部变量进行必要的重新赋值,其作用域将被限制在当前循环迭代中
如果不创建新的变量,所有的迭代(闭包goroutine)都将引用同一个变量,这可能导致值在goroutine之间被跳过或重复
有关https://go.dev/doc/faq#closures_and_goroutines更多信息,请访问www.example.com

相关问题