在Go语言中常见100问题-#58 Not understanding race problems中,举了一个存在数据竞争的例子,例子中有两个goroutine同时访问同一个变量,并且至少有一个对变量存在写操作。除了知道数据存在竞争外,我们还应该知道,在Go语言官方工具库中有一个工具可以检测数据竞争。一个常见的问题是忘记了这个工具的重要性并且没有启用它。本文将深入研究数据竞争检测器捕获警告内容的含义、如何开启竞争检测以及如何剔除不想进行竞争检测的文件。
Go语言中的竞争检测工具不是静态分析工具,也就是说该工具不是在编译期间生效,相反,它是一种查找程序运行时是否存在数据竞争的工具。要启用数据竞争检查,必须在编译或运行测试时添加-race选项,例如像下面这样:
$ go test -race ./...
添加-race编译选项后,编译器将在代码中插入检查代码(instrumentation指令),该指令会跟踪所有内存访问并记录它们发生的时间和方式。在程序运行时,竞争检测指令将监视数据竞争情况。但有一点需要了解,启动数据竞争检测在运行时是有开销的:
由于存在上述开销,建议仅在本地测试或CI期间启动竞争检查。在生产环境中,应该关闭竞争检查(或者只在金丝雀版本中使用)。此外,还有一点我们需要注意,无论执行上下文如何,Go数据竞争检查对同时执行的goroutine的数量有一个严格限制,这个数量值是8128。超过这个阈值,竞争检查器将停止工作。
如果检测到存在数据竞争,Go程序会产生警告。例如,下面的程序中存在数据竞争问题,因为变量i可以同时被多个goroutine进行读取和写入操作。
package main
import (
"fmt"
)
func main() {
i := 0
go func() { i++ }()
fmt.Println(i)
}
使用 -race 参数运行上述程序将产生如下警告信息:
==================
WARNING: DATA RACE
Write at 0x00c000026078 by goroutine 7:
main.main.func1()
/tmp/app/main.go:9 +0x4e
Previous read at 0x00c000026078 by main goroutine:
main.main()
/tmp/app/main.go:10 +0x88
Goroutine 7 (running) created at:
main.main()
/tmp/app/main.go:9 +0x7a
==================
为了输出的警告信息方便我们理解和排查,告警信息会记录如下信息:
「NOTE: 在内部实现上,竞争检测器采用了向量时钟(vector clocks)技术,这是一种用于确定事件偏序的数据结构(也用于分布式系统如数据库)。每个goroutine被创建都会创建一个向量时钟,然后instrumentation指令在每次内存访问和同步事件时更新向量时钟,通过比较向量时钟来判断是否存在数据竞争。」
需要注意,竞争检测器不会产生错误的上报,即不会出现实际没有数据竞争但检测器上报存在数据竞争的情况。因此,如果我们收到了警告信息,便可知道程序代码中包含有数据竞争。但是会存在这样的情况,代码实际上存在数据竞争,但是检测器没有检查出来,因为检测器依赖于代码运行,如果某些存在竞争情况没有运行到,便检测不出来。进行测试的时候注意两件事情:一是,竞争检测效果依赖于测试情况,所以对并发代码在数据竞争方面应该进行彻底运行测试,测试的越充分越能够检测出数据竞争;二是,考虑到有些数据竞争不容易检测出来,如果有一个测试来检测数据竞争,一个选择是将这个待测逻辑放在一个循环中,像下面的程序。这样,可以增加捕获可能的数据竞争机会。
func TestDataRace(t *testing.T) {
for i := 0; i < 100; i++ {
// Actual logic
}
}
在对程序测试的时候,如果特定测试文件会导致数据竞争产生,我们可以使用 !race 编译标签将其从竞争检测中排除。
//go:build !race
package main
import (
"testing"
)
func TestFoo(t *testing.T) {
// ...
}
func TestBar(t *testing.T) {
// ...
}
上述文件只会在当关闭竞争检测(即不带 -race选项时)才会构建。否则,整个文件不会被构建,也就不会执行文件里面的测试项。
总结:我们应该牢记,如果不是强制性的,强烈建议使用-race为带有并发的应用程序进行测试。通过-race选项启动数据竞争检测器。该检测器会检查我们的代码并捕获潜在的数据竞争。注意,启用检测器后会对内存和性能产生重要影响,因此它必须在特定的条件下使用,例如本地测试或CI环境。