【问题标题】:unclear on reasons why there is a race condition不清楚存在竞争条件的原因
【发布时间】:2020-05-22 14:46:05
【问题描述】:

问题涉及以下代码:

package main

import "fmt"

func main() {
    var counters = map[int]int{}
    for i := 0; i < 5; i++ {
        go func(counters map[int]int, th int) {
            for j := 0; j < 5; j++ {
                counters[th*10+j]++
            }
        }(counters, i)
    }
    fmt.Scanln()
    fmt.Println("counters result", counters)
}

这是我使用go run -race race.go 运行此代码时得到的输出

$ go run -race race.go
==================
WARNING: DATA RACE
Read at 0x00c000092150 by goroutine 8:
  runtime.mapaccess1_fast64()
      /usr/lib/go-1.13/src/runtime/map_fast64.go:12 +0x0
  main.main.func1()
      /tmp/race.go:10 +0x6b

Previous write at 0x00c000092150 by goroutine 7:
  runtime.mapassign_fast64()
      /usr/lib/go-1.13/src/runtime/map_fast64.go:92 +0x0
  main.main.func1()
      /tmp/race.go:10 +0xaf

Goroutine 8 (running) created at:
  main.main()
      /tmp/race.go:8 +0x67

Goroutine 7 (finished) created at:
  main.main()
      /tmp/race.go:8 +0x67
==================

==================
WARNING: DATA RACE
Read at 0x00c0000aa188 by main goroutine:
  reflect.typedmemmove()
      /usr/lib/go-1.13/src/runtime/mbarrier.go:177 +0x0
  reflect.copyVal()
      /usr/lib/go-1.13/src/reflect/value.go:1297 +0x7b
  reflect.(*MapIter).Value()
      /usr/lib/go-1.13/src/reflect/value.go:1251 +0x15e
  internal/fmtsort.Sort()
      /usr/lib/go-1.13/src/internal/fmtsort/sort.go:61 +0x259
  fmt.(*pp).printValue()
      /usr/lib/go-1.13/src/fmt/print.go:773 +0x146f
  fmt.(*pp).printArg()
      /usr/lib/go-1.13/src/fmt/print.go:716 +0x2ee
  fmt.(*pp).doPrintln()
      /usr/lib/go-1.13/src/fmt/print.go:1173 +0xad
  fmt.Fprintln()
      /usr/lib/go-1.13/src/fmt/print.go:264 +0x65
  main.main()
      /usr/lib/go-1.13/src/fmt/print.go:274 +0x13c

Previous write at 0x00c0000aa188 by goroutine 10:
  main.main.func1()
      /tmp/race.go:10 +0xc4

Goroutine 10 (finished) created at:
  main.main()
      /tmp/race.go:8 +0x67
==================
counters result map[0:1 1:1 2:1 3:1 4:1 10:1 11:1 12:1 13:1 14:1 20:1 21:1 22:1 23:1 24:1 30:1 31:1 32:1 33:1 34:1 40:1 41:1 42:1 43:1 44:1]
Found 2 data race(s)
exit status 66

这是我无法理解的。为什么会有比赛条件?我们不是只有一个 goroutine 可以读取/写入值吗?例如例程 0 将仅修改 counter[0]counters[4] 中的值,例程 1 将仅修改 counters[10]counters[14] 中的值,例程 2 将仅修改 counters[20]counters[24] 中的值等等。我在这里没有看到比赛条件。感觉好像我错过了什么。有人能对此有所了解吗?

仅供参考,我是新手。如果您能简化解释(如果可能的话),我将不胜感激。

【问题讨论】:

  • sync.Map(而不是常规映射和互斥锁)的一个特定用例是“多个 goroutine 读取、写入和覆盖不相交键集的条目”时,因此它可能在这种情况下值得研究。
  • 同意@hobbs,请记住sync.Map 是“无类型的”(它以interface{} 值运行)并且它的性能可能比受保护的地图更差通过特定条件下的互斥锁 - 那些与 sync.Map 不同的情况进行了优化。因此,编程时的经验法则几乎总是首先实现最易于理解的代码,然后才尝试消除性能瓶颈,如果有的话。

标签: go race-condition


【解决方案1】:

这对于数组(或切片)来说是正确的,但映射是一种复杂的数据结构,除其他外,它具有以下属性:

  • 您可以随时将存储在内存中的元素重新定位到内存中。
  • 地图最初是空的,在其中放置一个元素(在您的情况下显示为赋值)涉及到地图内部的大量操作。

此外,在像您这样的情况下——递增存储在地图中的整数——实际上是地图查找、递增和地图存储。 第一个和最后一个操作涉及按键查找。
现在考虑如果一个 goroutine 执行查找,同时另一个 goroutine 在执行映射存储时修改映射的内部状态会发生什么。


您可能想了解一下什么是关联数组,以及它通常是如何实现的。

【讨论】:

    【解决方案2】:

    我们不是只有一个 go 例程可以访问/读取值吗?

    你已经从@kostix 那里得到了一个很好的答案:当你向它添加元素时,地图的内部会被修改,所以认为 routine 0 会修改值 only 在counter[0]counters[4]

    但这还不是全部。

    您的代码中还有另一个数据竞争问题,它有点微妙,即使在测试中也可能很难发现。

    为了探索它,让我们摆脱@​​kostix 提到的“地图内部”问题,假设您的代码几乎完全相同,但有一个微小的变化:而不是使用map[int]int,假设您'正在使用 []int,初始化为至少长度为 56。像这样:

    // THERE'S ANOTHER RACE CONDITION HERE.
    
    // var counters = map[int]int{}
    var counters = make([]int, 56)
    for i := 0; i < 5; i++ {
        // go func(counters map[int]int, th int) {
        go func(counters []int, th int) {
            for j := 0; j < 5; j++ {
                counters[th*10+j]++
            }
        }(counters, i)
    }
    fmt.Scanln()
    fmt.Println("counters result", counters)
    

    这几乎是等效的,但摆脱了“地图内部”问题。目标是将焦点从“地图内部”转移到向您展示第二个问题。

    那里仍然存在竞争条件。顺便说一句,它也类似于在您得到的另一个答案中的第一个尝试解决方案中存在的竞争条件,它使用 sync.Mutex 但在方法仍然是错误的。

    这里的问题是在改变计数器的操作和从计数器读取的操作之间没有发生之前关系

    fmt.Scanln() 没有帮助:即使它允许您在它之前的代码(即,当for 循环启动 goroutines 时)和它之后的代码(即,fmt.Println())——这样你就可以想“好吧,我只是要等待'相当长的时间',然后再按 Enter”,这并不能消除竞争条件。

    这里的竞争条件源于“时间流逝”(即,您等待按 Enter)并未在对 counters 的写入和从中读取。

    happens-before 的概念对于避免数据竞争绝对是基础:只有当你能保证两个操作之间存在发生在之前的关系时,你才能保证不存在数据竞争。

    就像我提到的那样,“时间流逝”并不能确定“发生在之前”。要建立它,您可以使用多种替代方法之一,包括 syncatomic 包中的原语或通道等。

    虽然我可能会建议专注于研究频道,然后是 sync 包(sync.Mutexsync.WaitGroup 等),如果你确实想的话,也许只有在 atomic 包之后从权威来源阅读更多关于 happens before 的想法,这是链接:https://golang.org/ref/mem。但请注意,这是一罐令人讨厌的蠕虫。

    希望这里的这些 cmets 可以帮助您了解为什么在 Go 中遵循标准并发模式绝对是必不可少的。事情可能比第一眼看上去要微妙得多。

    最后,引用我上面分享的 Go Memory Model 链接:

    如果您必须阅读本文档的其余部分才能了解程序的行为,那您就太聪明了。

    别太聪明了。


    编辑:为了完成,这里是你可以解决问题的方法。

    解决方案有 2 个部分:(1) 确保地图没有并发修改; (2) 确保在对地图的所有更改和读取之间存在发生之前。

    对于 (1),您可以使用 sync.Mutex。写入前锁定,写入后解锁。

    对于(2),你需要保证主goroutine在所有修改完成后才能到达fmt.Println()。请记住:这里,after 并不意味着 “在以后的某个时间点”,它具体意味着必须建立一个发生在之前的关系。解决此问题的两种常见模式是使用通道或sync.WaitGroup。 WaitGroup 解决方案在这里可能更容易推理,所以我会使用它。

    var mu sync.Mutex                 // (A)
    var wg sync.WaitGroup             // (A)
    
    var counters = map[int]int{}
    
    wg.Add(5)                         // (B)
    
    for i := 0; i < 5; i++ {
        go func(counters map[int]int, th int) {
            for j := 0; j < 5; j++ {
                mu.Lock()             // (C)
                counters[th*10+j]++
                mu.Unlock()           // (C)
            }
            wg.Done()                 // (D)
        }(counters, i)
    } 
    wg.Wait()                         // (E)
    fmt.Scanln()
    fmt.Println("counters result", counters)
    

    (A) 您不需要初始化 Mutex 和 WaitGroup,因为它们的零值已经可以使用了。另外,你不需要让它们指向任何东西。

    (B) 你.Add(5)WaitGroup 的柜台,这意味着如果你在它上面.Wait(),它必须等待5 个.Done() 信号才能继续。这里的数字 5 是因为您要启动 5 个 goroutine,并且您需要在所有这些上所做的更改与主 goroutine 的 fmt.Println() 之间建立happens-before 关系。

    (C) 你.Lock().Unlock() Mutex 围绕修改map,以确保它们不会同时完成。

    (D) 就在每个 goroutine 终止之前,您调用 wg.Done(),这会减少 WaitGroup 的内部计数器。

    (E) 最后,你wg.Wait()。这个函数一直阻塞,直到wg 的计数器达到 0。这里是最重要的部分:WaitGroup 在对wg.Done() 的调用和wg.Wait() 调用的返回之间建立了happens-before 关系。换句话说,从内存一致性的角度来看,主 goroutine 可以保证看到所有 goroutine 对 map 执行的所有更改!

    最后,您可以使用 -race 运行该代码并开心!

    供您进一步探索:您可以将其替换为 sync.Map,而不是 map + sync.Mutex。但是sync.WaitGroup 仍然是必要的。尝试使用它编写一个解决方案,这可能是一个很好的练习。

    【讨论】:

    • 好答案!我现在明白为什么我的第一个例子仍然是错误的。谢谢!
    【解决方案3】:

    除了@kostix 答案。您必须知道多个 goroutine 不应在给定时间访问(写入/读取)同一资源。 因此,在您的实现中,您可能很容易遇到多个 goroutine 同时更新(读/写)同一个资源(即您的地图)的情况。

    应该发生什么?这个给定的映射键应该是哪个值?这就是所谓的race condition

    以下是对您的代码的一些潜在修复:

    使用Mutex

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    func main() {
        var counters = map[int]int{}
        var mutex = &sync.Mutex{}
        for i := 0; i < 3; i++ {
            go func(counters map[int]int, th int) {
                for j := 0; j < 3; j++ {
                    mutex.Lock()  // Lock the access to the map
                    counters[th*10+j]++
                    mutex.Unlock() // Release the access
                }
            }(counters, i)
        }
        fmt.Scanln()
        fmt.Println("counters result", counters)
    }
    

    输出:

    counters result map[0:1 1:1 2:1 10:1 11:1 12:1 20:1 21:1 22:1]
    

    使用sync.Map

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    func main() {
        var counters sync.Map
        for i := 0; i < 3; i++ {
            go func(th int) {
                for j := 0; j < 3; j++ {
                    if result, ok := counters.Load(th*10 + j); ok {
                        value := result.(int) + 1
                        counters.Store(th*10+j, value+1)
                    } else {
                        counters.Store(th*10+j, 1)
                    }
                }
            }(i)
        }
        fmt.Scanln()
        counters.Range(func(k, v interface{}) bool {
            fmt.Println("key:", k, ", value:", v)
            return true
        })
    }
    

    输出:

    key: 21 , value: 1
    key: 10 , value: 1
    key: 11 , value: 1
    key: 0 , value: 1
    key: 1 , value: 1
    key: 20 , value: 1
    key: 2 , value: 1
    key: 22 , value: 1
    key: 12 , value: 1
    

    【讨论】:

    • 在第一个 sn-p 上仍然存在竞争条件,即使用 sync.Mutex 的那个:如果您在读取计数器之前没有锁定互斥锁(即 fmt.Println(...) 行),不能保证对计数器的任何更新对主 goroutine 可见,因此对 fmt.Println(...) 函数可见。
    • @BrunoReis 在阅读了您的回答后,我现在知道为什么即使使用mutex,第一次尝试仍然是错误的。我不想在我的尝试中引入channels,因为它会改变函数的初始结构以及它们的调用方式。但你完全正确。感谢您的评论。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-03-10
    • 1970-01-01
    • 2022-01-23
    • 2018-10-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多