追加不是线程安全的?

Append not thread-safe?

提问人:Floating Sunfish 提问时间:5/24/2017 最后编辑:iczaFloating Sunfish 更新时间:11/17/2023 访问量:39855

问:

我注意到,如果我尝试在循环中使用 goroutines 附加到切片,在某些情况下我会得到缺失/空白数据:for

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()

有时,当我打印所有 s from 时,有些元素是空字符串 (),而其他时候,有些元素 from 中不存在。AttributeNamedestSlice""sourceSlicedestSlice

我的代码是否具有数据争用,这是否意味着对于多个 goroutine 并发使用来说,它不是线程安全的?append

go 并发 追加 slice goroutine

评论


答:

56赞 icza 5/24/2017 #1

在 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

解决方案很简单,使用同步。互斥锁以保护写入值: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从这个通道接收并执行追加。

另请注意,虽然切片标头不安全,但切片元素充当不同的变量,不同的切片元素可以同时写入而无需同步(因为它们是不同的变量)。请参阅是否可以同时写入不同的切片元素

评论

0赞 raine 3/25/2022
> 在 Go 中,没有值对于并发读/写是安全的,切片(即切片标头)也不例外。假。请参阅其他使用切片从 goroutines 收集结果的答案。
0赞 icza 3/25/2022
@raine 不,这是真的。切片元素和切片标头是不一样的。切片元素充当不同的变量,因此它们可以同时写入而不同步,但切片标头不能。因此,切片值(标头)对于并发读取和写入是不安全的!请参阅是否可以同时写入不同的切片元素
6赞 cody.tv.weber 7/12/2018 #2

为了给这个问题提供一个更新的解决方案,看起来 Go 已经发布了一个用于同步目的的新地图:

https://godoc.org/golang.org/x/sync/syncmap

评论

1赞 Floating Sunfish 7/13/2018
谢谢!我会调查的!
19赞 Mirian 4/23/2020 #3

这是一个相当古老的问题,但还有另一个小改进有助于摆脱互斥锁。您可以使用 index 添加到数组中。每个 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()

评论

0赞 Floating Sunfish 4/24/2020
事实上,当您事先知道收藏品的大小时,这是一个方便的解决方案!非常感谢您的分享!
0赞 Teemu 8/24/2021
我非常喜欢这个解决方案,并用它来解决我自己的问题。谢谢!我喜欢它,它不需要导入,而且它还通过分配一个固定的数组而不是在追加时动态增加它来优化一点。
6赞 lockwobr 7/20/2021 #4

问题已经得到解答,但我最喜欢的解决这个问题的方法是使用 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 包的人有所帮助。