go - 追加不是线程安全的?

标签 go concurrency append slice goroutine

我注意到,如果我尝试在 for 循环内使用 goroutines append 到 slice ,在某些情况下我会丢失/空白数据:

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 的并发使用不是线程安全的?

最佳答案

在 Go 中,没有任何值对于并发读/写是安全的, slice (slice headers)也不异常(exception)。

是的,您的代码存在数据竞争。使用 -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()

您也可以通过其他方式解决它,例如您可以使用一个 channel ,您可以在该 channel 上发送要追加的值,并让指定的 goroutine 从该 channel 接收并进行追加。

另请注意,虽然 slice header 不安全,但 slice 元素充当不同的变量,并且可以在不同步的情况下并发写入不同的 slice 元素(因为它们是不同的变量)。参见 Can I concurrently write different slice elements

关于go - 追加不是线程安全的?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44152988/

相关文章:

java - 使用 ScheduledExecutorService 以固定间隔并行运行多个线程

arrays - 将每个值动态 append 到 2D slice 中

php - php 中的 a+(读取/追加)与 a(追加)有何不同

Javascript,将大型非常大的表 append 到 DOM 的最佳方法

mongodb - 如何在 mongo-go-driver 中使用 “Let” 聚合选项?

go - 我想在 golang 中使用劫持,同时在客户端上得到无效响应

mongodb - 如何执行查找仅获取第一个元素

go - 使用函数通过方法满足接口(interface)

multithreading - 并行过滤惰性序列

java - 异步处理 = 跨线程 = 有效?