检查Golang程序中的数据竞争

race 数据竞争是并发程序中最常见的,也是最难发现的并发问题,所幸的是,Go内置了在一定程度上可以发现竞争问题。你可以在测试数据竞争检测器(data race detector),或者运行程序时使用-race开启数据竞争检测器,或者在编译程序时开启,编译好的二进制程序在运行时也可以开启数据竞争检测:

go
go test -race mypkg	// 测试mypkg包
go run -race mysrc.go	// 运行时测试源文件
go build -race mycmd	// 编译时测试
go install -race mypkg   // 安装时测试

还是以计数器的程序(TestCounter)为例,使用-race参数后会报出“WARNING:DATA RACE”错误。

go
func TestCounter() {
	var counter int64     // 计数值
	var wg sync.WaitGroup // 用來等待子goroutine全部执行完
	for i := 0; i < 64; i++ {
		wg.Add(1)
		go func() {
			for i := 0; i < 1000000; i++ { // 循环100万次
				counter++ // 计数值加1
			}
			wg.Done()
		}()
	}
	wg.Wait()
	if counter != 64000000 {
		fmt.Printf("counter should be 64000000, but got %d\n", counter)
	}
}

func main() {
	TestCounter()
}
go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c00000e238 by goroutine 8:
  main.TestCounter.func1()
      /Users/test/main.go:41 +0x40

Previous write at 0x00c00000e238 by goroutine 7:
  main.TestCounter.func1()
      /Users/test/main.go:41 +0x50

Goroutine 8 (running) created at:
  main.TestCounter()
      /Users/test/main.go:39 +0x6c
  main.main()
      /Users/test/main.go:54 +0x20

Goroutine 7 (running) created at:
  main.TestCounter()
      /Users/test/main.go:39 +0x6c
  main.main()
      /Users/test/main.go:54 +0x20
==================
==================
WARNING: DATA RACE
Write at 0x00c00000e238 by goroutine 8:
  main.TestCounter.func1()
      /Users/test/main.go:41 +0x50

Previous write at 0x00c00000e238 by goroutine 7:
  main.TestCounter.func1()
      /Users/test/main.go:41 +0x50

Goroutine 8 (running) created at:
  main.TestCounter()
      /Users/test/main.go:39 +0x6c
  main.main()
      /Users/test/main.go:54 +0x20

Goroutine 7 (running) created at:
  main.TestCounter()
      /Users/test/main.go:39 +0x6c
  main.main()
      /Users/test/main.go:54 +0x20
==================
counter should be 64000000, but got 58226219
Found 2 data race(s)
exit status 66

在执行的时候加上-race参数,可以看到Go数据竞争检测器发现了数据竞争的问题(WARNING:DATA RACE),并且把数据竞争的goroutine以及数据的创建、读/写信息都显示出来了,很方便我们分析数据竞争是怎么产生的。

如果你不想对某些函数进行数据竞争的检查,则可以使用条件编译,在文件的第一行加上以下一行:

// +build !race

当然,现在的Go版本使用新的条件编译语法:

//go:build !race

如果你想兼容以前的Go版本,这两种写法就都加上:

//go:build !race
// +build !race

注意,Go内建的数据竞争检测器并不会执行静态分析,而是在运行时对内存访问进行检查,只有针对运行的代码才有可能发现数据竞争问题,对于未被访问的代码,是发现不了数据竞争问题的。

所以在测试时并不能发现全部的数据竞争问题。通过运行开启了数据竞争检测器的编译好的二进制程序,是有可能发现更多的数据竞争问题的,但是也不能做到百分之百地发现,因为有些数据竞争可能只有在特定的条件下才会发生,而这个特定的条件什么时候存在并不能确定。另外,不要在生产环境中运行开启了数据竞争检测器的程序,因为进行数据竞争检查是有代价的。在开启了数据竞争检测器的情况下,内存占用可能增加5~10倍,而运行时间可能增加2~20倍。