介绍
共享数据竞争问题是并发系统中常见且难排查的问题.
什么是数据竞争?
当两个协程goroutine同时访问相同的共享变量,其中有一个执行了写操作,或两个都执行了写操作,就会出现数据竞争问题,导致数据异常.详情请参考Go内存模型详解:
如以下代码由于并发访问同一个map,存在数据冲突,一定概率导致数据异常,程序崩溃:
package main
import "fmt"
func main(){
c := make(chan bool)
m := make(map[string]string)
go func(){
m["1"] = "a" //运行时,这里有个协程对m进行写操作
c <- true
}()
m["2"] = "b" //主函数中也会对m进行写操作
<-c //根据通道先写后读的原则,这里会等待上面的go func执行完成
for k, v := range m {
fmt.Println(k, v)
}
}
多次执行后出现,程序崩溃,提示map并发写错误:
怎么使用数据冲突检测器?
在go中,已经内置数据冲突检测器,直接在go命令行添加参数-race即可,如以下4种方式:
- go test -race mypkg //测试mypkg包
- go run -race main.go //带冲突检测调试运行源代码main.go
- go build -race main.go //带冲突检测编译源代码
- go install -race mypkg//安装mypkg包
冲突报告格式
检测到冲突时,会按照以下格式打印冲突报告,它包含堆栈跟踪信息,以及协程编号,如:
➜ dataRace git:(master) ✗ go run -race main.go==================WARNING: DATA RACE //警告: 数据冲突Write at 0x00c000124180 by goroutine 7: //普通协程写操作 runtime .mapassign_faststr() /usr/local/go/src/runtime/map_faststr.go:202 +0x0 main.main.func1() /Users/xb/gitlab/go/go_core_program/sync/dataRace/main.go:9 +0x5d
Previous write at 0x00c000124180 by main goroutine: //主协程写操作 runtime.mapassign_faststr() /usr/local/go/src/runtime/map_faststr.go:202 +0x0 main.main() /Users/xb/gitlab/go/go_core_program/sync/dataRace/main.go:13 +0xcb
Goroutine 7 (running) created at: main.main() /Users/xb/gitlab/go/go_core_program/sync/dataRace/main.go:8 +0x9c==================2 b1 aFound 1 data race(s) //找到一处数据冲突exit status 66
运行选项
GORACE环境变量用于设置数据冲突检测选项, 在执行go程序前设置该值即生效,格式如下:
GORACE="选项1=值1 选项2=值2 ..."
包含以下常用选项:
- log_path (default stderr): 日志文件前缀,冲突检测结果存入log_path. pid . 特别的,如果配置为stdout则输出到标准输出,配置为stderr则输出到错误输出
- exitcode (default 66): 检测到冲突时,程序的退出状态码,可以自定义,默认为66
- strip_path_prefix (default “”): 为了使报告简洁,该选项用于去除文件路径中这些前缀
- history_size (default 1): 每个协程goroutine内存访问历史大小为32K*2**history_size 个元素,增加该选项值可以避免在报告中出现”恢复堆栈失败错误”,但是会增加内存开销
- halt_on_error (default 0): 用于控制程序遇到数据竞争时,是否退出,默认不会退出,只打印错误信息.
- atexit_sleep_ms (default 1000): 在主线程goroutine中等待多少毫秒后退出,默认1秒
排除单元测试
当你使用-race参数构建时,go命令定义了更多的构建标记,当你运行冲突检测时,你可以使用这些标记排除代码和单元测试,比如:
// +build !race 构建约束,用于排除有冲突的测试
package foo
// The test contains a data race. See issue 123.
func TestFoo(t *testing.T) {
// ... 包含数据冲突
}
// The test fails under the race detector due to timeouts.
func TestBar(t *testing.T) {
// ... 包含冲突检测超时导致失败的代码
}
// The test takes too long under the race detector.
func TestBaz(t *testing.T) {
// ... 包含在冲突检测下执行耗时太长的代码
}
使用冲突检测时注意事项
当使用go test -race做冲突检测时,检测器只会检测运行时的冲突,没有执行的代码块不会进行检测,如果你的单元测试是不完全覆盖,你需要使用go build -race构建一个完整的二进制包进行检测
典型数据冲突场景
- Race on loop counter 循环计数器冲突
package main
import (
"fmt"
"sync"
)
func main() {
//以下代码由于并发,同时获取值,存在冲突,所以i不会按照预期(012345)打印,比如打印55555,
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // Not the 'i' you are looking for.
wg.Done()
}()
}
}
解决办法:对变量拷贝一份出来,新的变量指向不同的内存地址
package main
import (
"fmt"
"sync"
)
func main() {
//读取本地拷贝值j,与i指向不同的地址,解决冲突
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
fmt.Println(j) // Good. Read local copy of the loop counter.
wg.Done()
}(i)
}
wg.Wait()
}
- 由于意外,共享了变量导致冲突
func ParallelWrite(data []byte) chan error {
res := make(chan error, 2)
f1, err := os.Create("file1")
if err != nil {
res <- err
} else {
go func() {
// This err is shared with the main goroutine, 这里的err变量与主线程共享了
// so the write races with the write below.
_, err = f1.Write(data)
res <- err
f1.Close()
}()
}
f2, err := os.Create("file2") // The second conflicting write to err.这里也在对err进行写操作
if err != nil {
res <- err
} else {
go func() {
_, err = f2.Write(data)
res <- err
f2.Close()
}()
}
return res
}
解决办法: 重新分配err变量
...
_, err := f1.Write(data)
...
_, err := f2.Write(data)
...
- 未加保护的全局变量
var service map[string] net .Addr
func RegisterService(name string, addr net.Addr) {
service[name] = addr
}
func LookupService(name string) net.Addr {
return service[name]
}
以上代码中,map在多个协程中并发中读写会导致冲突
解决方法: 使用互斥锁,保证同时只能读或者写
var (
service map[string]net.Addr
serviceMu sync.Mutex
)
func RegisterService(name string, addr net.Addr) {
serviceMu. Lock ()
defer serviceMu.Unlock()
service[name] = addr
}
func LookupService(name string) net.Addr {
serviceMu.Lock()
defer serviceMu.Unlock()
return service[name]
}
- 使用了不受保护的基本数据类型
基本数据类型,如bool, int, int64等也存在数据冲突,这种问题难以排查,一般都是由于非原子的内存访问引起的,如:
type Watchdog struct{ last int64 }
func (w *Watchdog) KeepAlive() {
w.last = time.Now().UnixNano() // First conflicting access. 写操作,与下面的读操作构成冲突
}
func (w *Watchdog) Start() {
go func() {
for {
time.Sleep(time.Second)
// Second conflicting access. 这里是读操作
if w.last < time.Now().Add(-10*time.Second).UnixNano() {
fmt.Println("No keepalives for 10 seconds. Dying.")
os.Exit(1)
}
}
}()
}
解决方法:使用通道或者互斥锁mutex, 也可以使用无锁的sync/atomic包,如:
type Watchdog struct{ last int64 }
func (w *Watchdog) KeepAlive() {
atomic.StoreInt64(&w.last, time.Now().UnixNano()) //使用原子包存储方法
}
func (w *Watchdog) Start() {
go func() {
for {
time.Sleep(time.Second)
if atomic.LoadInt64(&w.last) < time.Now().Add(-10*time.Second).UnixNano() { //使用原子包的读取方法
fmt.Println("No keepalives for 10 seconds. Dying.")
os.Exit(1)
}
}
}()
}
- 未同步的发送和关闭操作,如:
c := make(chan struct{}) // or buffered channel 这里也可以使用带缓冲的通道演示
// The race detector cannot derive the happens before relation
// for the following send and close operations. These two operations 下面的通道发送和关闭操作没有进行同步,导致冲突
// are unsynchronized and happen concurrently.
go func() { c <- struct{}{} }()
close(c)
解决方法:通道关闭前,增加一个读取操作,完成同步
c := make(chan struct{}) // or buffered channel
go func() { c <- struct{}{} }()
<-c //根据Go语言内存模型,一个通道上写操作发生在读操作之前,所以这里读取时,写已经完成,完成了通道同步
close(c)
- 单例模式场景也使用锁避免冲突
package main
import (
"fmt"
"sync"
"sync/atomic"
)
//定义单例结构体
type singleton struct {}
var(
instance *singleton
initialized uint32 //初始化标志,用于标识是否已经被初始化
mu sync.Mutex //互斥锁对象
)
func Instance() *singleton{
if atomic.LoadUint32(&initialized)==1{ //如果实例已经初始化,直接返回
return instance
}
//如果没有实例化,则用锁同步执行下面的代码,即同一时间只能有一个协程进入执行以下代码块
mu.Lock()
defer mu.Unlock()
if instance==nil{
defer atomic.StoreUint32(&initialized, 1)
instance = &singleton{}
}
return instance
}
func main(){
mySingleton := Instance()
fmt.Printf("单例模式得到的对象:%v\n", mySingleton)
}
- 与单例模式类似的,用锁实现某个方法只调用一次(DoOnce)
package main
import (
"sync"
"sync/atomic"
)
type Once struct{
m sync.Mutex
done uint32
}
//传入一个回调函数,保证只执行一次该回调函数
func(o *Once)Do(f func()){
if atomic.LoadUint32(&o.done) == 1 {
return
}
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f() //回调函数
}
}
数据冲突检测器当前支持的系统
- linux/amd64
- linux/ppc64le
- linux/arm64
- freebsd/amd64
- netbsd/amd64
- darwin/amd64
- windows/amd64
运行时开销
开启冲突检测,通常程序的内存使用会增加5~10倍,执行耗时增加2~20倍.
注意事项: 数据冲突检测器为每个defer和recover语句分配额外8字节,该内存直到协程退出才会释放,这意味着如果你有一个长时间运行的协程goroutine,它会周期性的调用defer和recover,导致程序内存使用持续增长,且这些内存分配不会显示在runtime.ReadMemStats(运行时读内存统计)和runtime/pprof(运行时性能调试工具pprof统计)中.
参考文档:
Go语言高级编程 (Advanced Go Programming)