竞态检测器能抓到什么、抓不到什么、要花多少钱?这篇用几个小程序实测:它基于 happens-before 而不是碰运气,但只检查实际执行过的路径;开销在不同类型的程序上差很多。
counter = 2000
==================
WARNING: DATA RACE
Read at 0x00010498e490 by goroutine 17:
main.main.func1()
<gtmp>/rc1/main.go:42 +0x88
Previous write at 0x00010498e490 by goroutine 18:
main.main.func1()
<gtmp>/rc1/main.go:42 +0xa0
Goroutine 17 (running) created at:
main.main()
<gtmp>/rc1/main.go:42 +0x5f0
Goroutine 18 (finished) created at:
main.main()
<gtmp>/rc1/main.go:42 +0x5f0
==================
==================
WARNING: DATA RACE
Write at 0x00010498e490 by goroutine 17:
main.main.func1()
<gtmp>/rc1/main.go:42 +0xa0
Previous write at 0x00010498e490 by goroutine 18:
进程退出码:66
同一份源码不加 -race 编译:counter = 1367(退出码 0,没有任何提示)两个 goroutine 各做 1000 次 counter++,带 -race 的这次最后打印 2000,看起来没丢更新,但检测器报告了读写竞争并给出两个 goroutine 的创建位置;不带 -race 的版本这次打印 1367。竞争不是“结果错了”,而是没有同步关系。
[sleepordered] 退出码 66
读到 42
WARNING: DATA RACE
Read at 0x000102ee6490 by main goroutine:
Previous write at 0x000102ee6490 by goroutine 17:
[atomicmixed] 退出码 66
普通读到 1
WARNING: DATA RACE
Read at 0x000104d1a498 by main goroutine:
Previous write at 0x000104d1a498 by goroutine 17:
[readonly] 退出码 0:只读,无竞争time.Sleep 让读发生在写之后几十毫秒,检测器仍然报告竞争,因为两者之间没有锁、channel 或原子操作建立 happens-before。原子写配普通读也同样被报。只读共享则不报。
[不触发] 退出码 0:done [TRIGGER=1] 退出码 66:done | WARNING: DATA RACE
竞争分支只在设了环境变量时才执行:不设时即使存在竞争也是退出码 0。所以要用测试覆盖到并发路径,并在 CI 里跑 go test -race(本文没有实验 go test)。
GORACE=exitcode=7:退出码 7 GORACE=halt_on_error=1:退出码 66;出现 "WARNING: DATA RACE" 1 次;最后一行 "=================="
GORACE=exitcode=7 把发现竞争时的退出码从默认的 66 改成 7;GORACE=halt_on_error=1 让进程在第一次竞争报告后就退出,实验里只出现 1 次报告。
-race版本:10000 个 goroutine 同时存活,正常结束(退出码 0) 普通版本:10000 个 goroutine 同时存活,正常结束(退出码 0) -race版本:100000 个 goroutine 同时存活,正常结束(退出码 0) 普通版本:100000 个 goroutine 同时存活,正常结束(退出码 0)
我听说旧版本的竞争检测器对同时存活的 goroutine 有 8128 的上限,在这台机器上 10 万个都没问题,所以不能写它仍然存在;至于是哪个版本取消的,我没有查。
内存密集的循环(数组 1M×40 遍) 普通 :耗时 0.034 / 0.034 / 0.035 s;内存中位 14 MB
内存密集的循环(数组 1M×40 遍) -race:耗时 1.057 / 1.062 / 1.064 s;内存中位 48 MB
→ 耗时中位数比 31.2 倍;内存中位数比 3.6 倍
8 个 goroutine 争锁各 20 万次 普通 :耗时 0.141 / 0.148 / 0.164 s;内存中位 5 MB
8 个 goroutine 争锁各 20 万次 -race:耗时 0.505 / 0.514 / 0.519 s;内存中位 32 MB
→ 耗时中位数比 3.5 倍;内存中位数比 5.8 倍访存密集的循环慢了 31.2 倍、内存多 3.6 倍;主要是争锁的程序只慢 3.5 倍,但内存多了 5.8 倍。所以别用某一个“慢 5 到 10 倍”的口号估算,要在自己的程序上量。