Race Conditions 是最隐晦而且难以捉摸的编程错误之一,一般会在代码部署到生产之后很长时间才会发作,但是如果要通过 Golang 进行并发编程,那么就会很容易遇到。
Go 的并发机制使得编写干净并发代码变得容易,但它们并不能防止竞态条件。
示例
当两个协程同时访问相同的变量并且访问中的至少一个是写入时,就会发生数据竞争。
package main
import (
"fmt"
"time"
)
func main() {
a := 1
go func() {
a = 2
}()
fmt.Println("a is ", a)
time.Sleep(1 * time.Second)
}
这里 main 和另一个匿名协程,都会访问 a 变量,并且有对 a 的写操作,所以会触发竞态条件。
使用方式
在 Golang 中自带的 race 的检测,在命令行中,可以使用 -race
参数。
$ go test -race mypkg
$ go run -race mysrc.go
$ go build -race mycmd
$ go install -race mypkg
环境变量
另外,还可以使用环境变量 GORACE
来设置一些参数,其格式为:
GORACE="option1=val1 option2=val2"
可选的参数有:
log_path
输出结果的写入路径,默认 stderr ;exitcode
在检测到race后退出时的状态码,默认为 66 ;strip_path_prefix
输出结果中去掉文件夹前缀,这样更简洁,默认空字符串;history_size
每个协程的访问记录保存大小,可设置为32K * 2 ** history_size
,默认是 1 ;halt_on_error
是否程序在触发了第一次 data race 就退出,默认是 0 ;
增加 history_size
大小可以避免出现 failed to restore the stack
的错误,但是代价是增加内存使用量。
$ GORACE="strip_path_prefix=/tmp/test/" go run -race
a is 1
==================
WARNING: DATA RACE
Write at 0x00c4200a4010 by goroutine 6:
main.main.func1()
main.go:11 +0x38
Previous read at 0x00c4200a4010 by main goroutine:
main.main()
main.go:13 +0x88
Goroutine 6 (running) created at:
main.main()
main.go:10 +0x7a
==================
Found 1 data race(s)
exit status 66
从报告可以看到一处 data race,在 main.go:11
处有协程 #6 对 0x00c4200a4010
的写入,在 main.go:13
处有 main goroutine
对同一变量 0x00c4200a4010
的读取操作。
规避手段
遇到 race 后的解决办法一般来说有三种:channels、锁和原子操作。
package main
import (
"fmt"
"time"
)
func main() {
a := 1
done := make(chan int, 1)
go func() {
a = 2
done <- 1
}()
<-done
fmt.Println("a is ", a)
time.Sleep(1 * time.Second)
}
上述是通过管道来保证数据同步。