追加不是线程安全的?

问题描述 投票:0回答:5

我注意到,如果我尝试在

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

有时,当我打印

AttributeName
中的所有
destSlice
时,某些元素是空字符串 (
""
),而其他时候,
sourceSlice
中的某些元素不存在于
destSlice
中。

我的代码是否存在数据竞争,这是否意味着

append
对于多个 goroutine 并发使用来说不是线程安全的?

go concurrency append slice goroutine
5个回答
56
投票

在 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 从该通道接收并执行附加操作。

还要注意,虽然切片头不安全,但切片元素充当不同的变量,并且不同的切片元素可以在不同步的情况下同时写入(因为它们是不同的变量)。请参阅我可以同时写入不同的切片元素吗


19
投票

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

6
投票

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

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


6
投票

问题已得到解答,但我最喜欢的解决此问题的方法是使用 errgroupdocs 中的示例之一就是这个确切的问题以及错误处理的一个很好的补充。

以下是文档中示例的主要内容:

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 包的人有所帮助。


0
投票

我已经在 collections 包上工作了一段时间,它提供了 List 接口,以更方便的方式处理切片,以及线程安全的 SafeList 实现。

我认为你可以使用 Clone() 或 Map() 方法来轻松实现你的目标。

欢迎在github上讨论或向我发帖

© www.soinside.com 2019 - 2024. All rights reserved.