竞争状态

数据竞争状态 是两个多个运行中的如线程和 goroutines 试图控制或修改共享的资源或程序变量。严格讲,数据竞争发生在当两个或多个指令访问同一个内存地址,它们中至少有一个在此执行写操作。

当运行或构建 Go 源文件时使用 -race 标志会开启 Go 竞争监视器,它会使编译器创建一个典型的可执行文件的修改版。这个修改版可以记录所有对共享变量的访问以及发生的同步事件,包括调用 sync.Mutexsync.WaitGroup。分析相关事件后,竞争监视器打印一份报告来帮助您识别潜在问题,这样您就可以修正它们了。

请看下面的 Go 代码,它保存为 racec.go。这个程序分三部分来介绍。

raceC.go 的第一部分如下:

  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. "strconv"
  6. "sync"
  7. )
  8. func main() {
  9. arguments := os.Args
  10. if len(arguments) != 2 {
  11. fmt.Println("Give me a natural number!")
  12. os.Exit(1)
  13. }
  14. numberGR, err := strconv.Atoi(os.Args[1])
  15. if err != nil {
  16. fmt.Println(err)
  17. return
  18. }

raceC.go 的第二段代码如下:

  1. var waitGroup sync.WaitGroup
  2. var i int
  3. k := make(map[int]int)
  4. k[1] = 12
  5. for i = 0; i < numGR; i++ {
  6. waitGroup.Add(1)
  7. go func() {
  8. defer waitGroup.Done()
  9. k[i] = i
  10. }()
  11. }

raceC.go 的其余代码如下:

  1. k[2] = 10
  2. waitGroup.Wait()
  3. fmt.Println("k = %v\n", k)
  4. }

好像许多 goroutines 同时访问 k map 还不够,我们在调用 sync.Wait() 函数前再添加一个访问 k map 的语句。

如果您执行 raceC.go,会获得如下没有任何警告或错误信息的输出:

  1. $go run raceC.go 10
  2. k = map[int]int{7:10, 2:10, 10:10, 1:12}
  3. $go run raceC.go 10
  4. k = map[int]int{2:10, 10:10, 1:12, 8:8, 9:9}
  5. $go run raceC.go 19
  6. k = map[int]int{10:10, 1:12, 6:7, 7:7, 2:10}

如果您仅执行一次 raceC.go,当打印 k map 内容的时候尽管您没有得到期望的,但一切看起来正常。然而,多次执行 raceC.go 告诉我们这有些错误,主要是每次执行产生不同的输出。

如果我们决定使用 Go 竞争监视器分析 raceC.go,我们可以获得更多信息及意外输出:

""

竞争监视器发现两处数据竞争。每个都在它的输出里用 WARNING: DATA RACE 消息开头。

第一个 数据竞争 发生在 main.main.func1() 内,它由一个goroutine 执行的 for 循环调用。这的问题由 Previous write 消息表示。检查相关代码后,很容易看到实际问题是匿名函数没带参数,意思是在 for 循环中使用的 i 值不同准确识别,因为这是个写操作,在 for 循环内不断改变。

第二处数据竞争信息是 Write at 0x00c420074180 by goroutine 7。如果您阅读相关输出,会看到这个数据竞争是关于写操作的,并且至少有两个 goroutines 在执行。因为这两个 goroutine 有相同的名字(main.main.func1()),这表明我们谈论的是同一个 goroutine。这两个 goroutine 试图写同一个变量,这就是数据竞争状态!

Go 用 main.main.func1() 记号命名一个内部地匿名函数。如果您有不同地匿名函数,它们的名字同样会不同

您可以问之际,现在为了修正这两个数居竞争引起的问题我能做什么?

好的,您可以重写 raceC.gomain() 函数如下:

  1. func main() {
  2. arguments := os.Args
  3. if len(arguments) != 2 {
  4. fmt.Println("Give me a natural number!")
  5. os.Exit(1)
  6. }
  7. numGR, err := srconv.Atoi(os.Args[1])
  8. if err != nil {
  9. fmt.Println(err)
  10. return
  11. }
  12. var waitGroup syncWaitGroup
  13. var i int
  14. k := make(map[int]int)
  15. k[1] = 12
  16. for i = 0; i < numGR; i++ {
  17. waitGroup.Add(1)
  18. go runc(j int) {
  19. defer waitGroup.Done()
  20. aMutex.Lock()
  21. k[j] = j
  22. aMutex.Unlock()
  23. }(i)
  24. }
  25. waitGroup.Wait()
  26. k[2] = 10
  27. fmt.Printf("k = %#v\n", k)
  28. }

aMutex 变量是一个定义在 main() 函数外的全局 sync.Mutex 变量,它可以在程序的任何地方访问到。尽管这不必要,不过有这样一个全局变量可以免得您总是把它传给函数。

把这个新版本 raceC.go 保存为 noRaceC.go 并执行它产生如下输出:

  1. $go run noRaceC.go 10
  2. k = map[int]int{1:1, 0:0, 5:5, 3:3, 6:6, 9:9, 2:10, 4:4, 7:7, 8:8}

用 Go 竞争监测器运行 noRaceC.go 产生如下输出:

  1. $go run -race noRaceC.go 10
  2. k = map[int]int{5:5, 7:7, 9:9, 1:1, 0:0, 4:4, 6:6, 8:8, 2:10, 3:3}

注意当访问 k map 时,您需要一个锁机制。如果您没有使用这样的机制并且只修改了由 goroutine 执行的匿名函数的实现的话,您会从 go run noRaceC.go 获得如下输出:

""

这个根本问题是显而易见的:并发 map 写操作