【问题标题】:Why is this function not thread safe in golang?为什么这个函数在 golang 中不是线程安全的?
【发布时间】:2023-03-27 20:45:01
【问题描述】:

这是我提到的代码:

// this is inside some method which has return signature like this: (*Data, error)
mapStore := make(...)
resSlice := make(...)
wg := new(sync.WaitGroup)
ec := make(chan error)
for keyString, sliceValue := range myMap {
     wg.Add(1)

     keyString := keyString
     sliceValue := sliceValue

     go func() {
          err := func(keyString string, sliceValue []Value, wg *sync.WaitGroup) error {
                defer wg.Done()
                res, err := process(keyString, sliceValue)
                if err != nil {
                      return errors.Wrapf(err, "wrong")
                }
                if res == nil {
                      return nil
                }
                if res.someData != nil {
                      mapStore[*res.someData] = append(mapStore[*res.someData], res)
                      return nil
                }
                resSlice := append(resSlice, res)
                return nil
               }(keyString, sliceValue, wg)
               if err != nil {
                     ec <- err
                     return
               }
          }()
     }
}
wg.Wait()

select {
case err := <- ec:
      return nil, err
default:
      return resSlice, nil
}

我被告知由于某种原因这不是线程安全的,但我不确定在哪里。我认为这是处理ec 中的错误的问题,但希望得到一些帮助!

【问题讨论】:

  • 你能提供一个minimum reproducible example吗?这个 sn-p 缺少很多有助于调试的信息。我怀疑你的问题是当你这样做 mapStore[*res.someData] 时,你正在修改一个你也在多个 go-routines 中迭代的地图。
  • 变量mapStoreresSlice同时修改。
  • 任何时候你认为存在并发问题但你不知道在哪里,that's what the race detector is for

标签: go parallel-processing thread-safety channel goroutine


【解决方案1】:

我还没有全部分析过,但是从多个goroutines修改mapStore肯定是不安全的:

mapStore[*res.someData] = append(mapStore[*res.someData], res)

但作为一个起点,在race detector 下运行它。它会为你发现很多问题。

这显然也不安全:

resSlice := append(resSlice, res)

但它也并不完全符合您的想法。这会创建一个名为resSlice 的新局部变量,它会遮蔽外部变量,但它也会修改外部变量(参见下面的 cmets)。除了两个东西可能同时尝试追加并发生冲突之外,append 可以在需要重新分配时将整个切片移动到内存中,因此即使您在其周围加了锁,这也会导致线程安全问题。

通常,您不想让每个 goroutine 更新某个中心变量,而是希望每个 goroutine 将其结果传递回一个通道。然后让主函数收集所有值并更新变量。有关示例,请参阅Go Concurrency Patterns: Pipelines and cancellation

【讨论】:

  • "但它也修改了外层" -> "但它也可能修改了外层" - 如果append 需要增长数组,它不会' t 修改输入切片(这就是 append 有返回值的原因)。
  • @Adrian 我指的是它会修改内容和长度,这可能会导致冲突,因为这发生在多个 goroutine 上。无论是否必须重新分配,它都会这样做。 (我错过了什么吗?)
  • (啊,你是说你可以得到两份副本,一份经过修改,一份未修改。对吧?)
  • 完全正确 - append 的行为完全取决于它是否必须扩展数组。在某些情况下,他们会以两个不同的切片结束,在某些情况下,他们会以数据竞争告终。这是一个有争议的问题,因为本地阴影几乎可以肯定是一个错误,只是指出 append 并不总是修改它作为输入获得的切片。
猜你喜欢
  • 2015-07-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-07
  • 2018-10-25
  • 2018-08-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多