go竞争探测器文档:Introducing the Go Race Detector
竞争条件是最隐蔽和难以捉摸的编程错误之一。它们通常会导致不稳定和神秘的故障,通常是在代码已部署到生产环境很久之后。虽然 Go 的并发机制使编写干净的并发代码变得容易,但它们并不能防止竞争条件。需要谨慎、勤奋和测试。工具可以提供帮助。
race detector 与 go 工具链集成。设置 -race 命令行标志后,编译器会使用记录访问内存的时间和方式的代码来检测所有内存访问,同时运行时库会监视对共享变量的非同步访问。当检测到这种“不雅”行为时,会打印一条警告。 (有关算法的详细信息,请参阅本文。)
由于其设计,竞争检测器只能在竞争条件实际被运行代码触发时检测到竞争条件,这意味着在实际工作负载下运行启用竞争的二进制文件非常重要。但是,启用竞争的二进制文件可以使用十倍的 CPU 和内存,因此始终启用竞争检测器是不切实际的。摆脱这种困境的一种方法是在启用竞争检测器的情况下运行一些测试。负载测试和集成测试是很好的候选者,因为它们倾向于执行代码的并发部分。另一种使用生产工作负载的方法是在正在运行的服务器池中部署一个支持竞争的实例。
竞争检测器与 Go 工具链完全集成。要在启用竞争检测器的情况下构建代码,只需将 -race 标志添加到命令行:
$ go test -race mypkg // test the package
$ go run -race mysrc.go // compile and run the program
$ go build -race mycmd // build the command
$ go install -race mypkg // install the package
使用竞争检测器
要亲自试用竞争检测器,请将此示例程序复制到 racy.go 中:
package main
import "fmt"
func main() {
done := make(chan bool)
m := make(map[string]string)
m["name"] = "world"
go func() {
m["name"] = "data race"
done <- true
}()
fmt.Println("Hello,", m["name"])
<-done
}
然后在启用竞争检测器的情况下运行它:
$ go run -race racy.go
发现上面代码存在竞争
Examples
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
start := time.Now()
var t *time.Timer
t = time.AfterFunc(randomDuration(), func() {
fmt.Println(time.Now().Sub(start))
t.Reset(randomDuration())
})
time.Sleep(5 * time.Second)
}
func randomDuration() time.Duration {
return time.Duration(rand.Int63n(1e9))
}
这看起来是合理的代码,但在某些情况下它会以令人惊讶的方式失败:
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x8 pc=0x41e38a]
goroutine 4 [running]:
time.stopTimer(0x8, 0x12fe6b35d9472d96)
src/pkg/runtime/ztime_linux_amd64.c:35 +0x25
time.(*Timer).Reset(0x0, 0x4e5904f, 0x1)
src/pkg/time/sleep.go:81 +0x42
main.func·001()
race.go:14 +0xe3
created by time.goFunc
src/pkg/time/sleep.go:122 +0x48
这里发生了什么?在启用竞争检测器的情况下运行程序更具启发性:
==================
WARNING: DATA RACE
Read by goroutine 5:
main.func·001()
race.go:16 +0x169
Previous write by goroutine 1:
main.main()
race.go:14 +0x174
Goroutine 5 (running) created at:
time.goFunc()
src/pkg/time/sleep.go:122 +0x56
timerproc()
src/pkg/runtime/ztime_linux_amd64.c:181 +0x189
==================
race detector 显示了问题:来自不同 goroutines 的变量 t 的不同步读写。如果初始计时器持续时间非常短,则计时器函数可能会在主 goroutine 为 t 赋值之前触发,因此对 t.Reset 的调用是用 nil t 进行的。
清风个人见解:
- 这个错误提示是 Go 语言中的数据竞争检测器(Data Race Detector)输出的。它表示在程序运行过程中,有两个 goroutine 分别读取和写入了同一个变量,并且它们之间没有进行同步操作,从而导致了数据竞争问题。
- 具体来说,在这个错误提示中,goroutine 5 在 main.func·001() 函数中读取了某个变量的值,而 goroutine 1 在 main.main() 函数中写入了该变量的值。由于这两个 goroutine 没有进行同步操作,因此就可能会出现读写不同步的问题,从而引发数据竞争。
- 为了解决这个问题,可以使用 Go 语言提供的同步原语(如互斥锁、条件变量等)来保护对共享变量的访问。具体来说,可以在写入变量的代码块中获取一个互斥锁,然后再进行写入操作,最后释放锁。在读取变量的代码块中也需要获取相同的互斥锁,以确保读取到的是最新的值。这样就能够避免多个 goroutine 同时访问共享变量,从而解决了数据竞争问题。
为了修复竞争条件,我们更改代码以仅从主 goroutine 读取和写入变量 t:
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
start := time.Now()
reset := make(chan bool)
var t *time.Timer
t = time.AfterFunc(randomDuration(), func() {
fmt.Println(time.Now().Sub(start))
reset <- true
})
for time.Since(start) < 5*time.Second {
<-reset
t.Reset(randomDuration())
}
}
func randomDuration() time.Duration {
return time.Duration(rand.Int63n(1e9))
}
$ go run -race main.go
963.1441ms
1.0586156s
1.7398158s
1.9771881s
2.278302s
2.8400051s
3.4801887s
3.8141354s
4.0054143s
4.4983853s
5.2601257s
5.5267243s