在 不理解竞争问题 中,我们定义了一个数据竞争,即两个 goroutine 同时访问同一个变量,并且至少有一个写入该变量。除此之外,我们应该知道,在 Go 中,存在一个标准工具来帮助检测数据竞争。一个常见的错误是忘记了这个工具的重要性并且没有启用它。本节将深入研究竞争检测器捕获的内容、如何使用它以及限制。
在 Go 中,竞争检测器不是编译期间会发生的静态分析工具;相反,它是一种查找运行时发生的数据竞争的工具。
要启用它,我们必须在编译或运行测试时设置 ‑race
命令行,例如:
$ go test -race ./...
启用后,编译器将检测代码以检测数据竞争。Instrumentation 是指编译器添加额外的指令。在这里,跟踪所有内存访问并记录它们发生的时间和方式。然后,在运行时,竞争检测器将监视数据竞争。但是,我们应该记住启用竞争检测器的运行时开销:
启用后,编译器将检测代码以检测数据竞争。检测是指编译器添加额外的指令。在这里,跟踪所有内存访问并记录它们发生的时间和方式。然后,在运行时,竞争检测器将监视数据竞争。但是,我们应该记住启用竞争检测器的运行时开销:
- 内存使用量可能会增加 5‑10 倍
- 执行时间可能会增加 2‑20 倍
由于这种开销,通常建议仅在本地测试或 CI 期间启用竞争检测器。在生产环境中,我们应该避免它,或者只在金丝雀(少量用户使用)版本的情况下使用它。
但是,我们应该记住,无论执行上下文如何,Go 竞争检测器对同时执行的 goroutine 的数量都有一个严格的限制:8128。超过这个阈值,竞争检测器将停止。
如果检测到竞争,Go 将发出警告。例如,此示例包含数据竞争,因为可以同时访问 i
以进行读取和写入:
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
==================
让我们确保我们在阅读这些信息时感到自在。Go 总是记录:
- 并发 goroutine 有哪些:主 goroutine 和 goroutine 7
- 访问发生在代码中的位置:第 9 行和第 10 行
- 这些 goroutines 是什么时候创建的:在
main()
中创建了goroutine 7
Note 在内部,竞争检测器使用矢量时钟,这是一种用于确定事件的部分顺序的数据结构(也用于数据库等分布式系统)。每个 goroutine 创建都会导致创建一个向量时钟。然后,仪器在每次内存访问和同步事件时更新矢量时钟。然后,它比较矢量时钟以检测潜在的数据竞争。
我们应该注意,竞争检测器无法捕获误报(不是真实的数据竞争)。因此,如果我们收到警告,我们可以知道我们的代码包含数据竞争。
相反,它在某些情况下可能会导致误报:缺少实际的数据竞争。测试方面需要注意两件事。首先,竞争检测器只能和我们的测试一样好。因此,我们应该确保针对数据竞争对并发代码进行彻底测试。此外,考虑到可能的假阴性,如果我们确实有一个测试来检查数据竞争,一个选择可能是将这个逻辑放在一个循环中。这样,我们可以增加捕获可能的数据竞争的机会:
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
标志为利用并发的应用程序运行测试。它允许启用竞争检测器,该检测器检测我们的代码以捕获潜在的数据竞争。启用后,它会对内存和性能产生重要影响;因此它必须在特定条件下使用,例如本地测试或 CI。
下一节将讨论与执行模式相关的两个标志:parallel
和 shuffle
。