Go Data race and Vector clock
Data race 数据竞态(争)
data race是指多个运行分支(线程)同时访问某共享资源,并且有写操作的情况。此时该共享资源因同时发生读写,其数据状态不确定。编码时应杜绝数据竞态问题的产生。
如下是一个简单的例子,global 全局变量初始状态为0,我们启动两个协程,同时对global进行自增1的操作,各执行1万次。我们的假想是该代码执行完打印global时,值为20000。但并非如此,可能偶尔是20000。当前运行结果:16449。
package main
import (
"fmt"
"time"
)
var global = 0
func main() {
go func() {
for i := 0; i < 10000; i++ {
global++
}
}()
for i := 0; i < 10000; i++ {
global++
}
time.Sleep(time.Second)
fmt.Println(global)
}
造成这种问题的根源是对global的自增操作不具有原子性。并且有多个协程对没有锁保护的资源进行访问。
使用 -race 子命令检测数据竞争
这里使用go run -race main.go
来检测main.go文件在执行过程中是否有竞态问题。-race 不仅可以用在 run 子命令后,还可用在 build , test 之后。
...\main>go run -race main.go
==================
WARNING: DATA RACE
Read at 0x000000638610 by goroutine 7:
main.main.func1()
.../main/main.go:13 +0x32
Previous write at 0x000000638610 by main goroutine:
main.main()
.../main/main.go:18 +0x5d
Goroutine 7 (running) created at:
main.main()
.../main/main.go:11 +0x30
==================
20000
Found 1 data race(s)
exit status 66
可以看出,-race子命令报告了代码Found 1 data race(s)
,并且标记了代码发生数竞争的位置。
data race 解决办法
当你的代码存在数据竞争时,一定要解决它。解决办法有几种:1. 对共享资源加锁;2.操作共享资源时使用原子操作(适用范围比较小);3.使用通道。其目的都是保证读写不在同一时刻发生,产生互斥的效应。
加锁
package main
import (
"fmt"
"sync"
"time"
)
var global = 0
var globalMu = sync.Mutex{}
func main() {
go func() {
for i := 0; i < 10000; i++ {
globalMu.Lock()
global++
globalMu.Unlock()
}
}()
for i := 0; i < 10000; i++ {
globalMu.Lock()
global++
globalMu.Unlock()
}
time.Sleep(time.Second)
fmt.Println(global)
}
再次使用go run -race main.go
,检测运行中代码是否存在竞态问题:
...\main>go run -race main.go
20000
可以看出,没有竞态问题的发生了。
原子操作
此时将global修改为int64类型。
package main
import (
"fmt"
"sync/atomic"
"time"
)
var global int64 = 0
func main() {
go func() {
for i := 0; i < 10000; i++ {
atomic.AddInt64(&global, 1)
}
}()
for i := 0; i < 10000; i++ {
atomic.AddInt64(&global, 1)
}
time.Sleep(time.Second)
fmt.Println(global)
}
通道
package main
import (
"fmt"
"time"
)
var global int64 = 0
func main() {
ch := make(chan struct{}, 1)
go func() {
for i := 0; i < 10000; i++ {
ch <- struct{}{}
global++
<-ch
}
}()
for i := 0; i < 10000; i++ {
ch <- struct{}{}
global++
<-ch
}
time.Sleep(time.Second)
fmt.Println(global)
close(ch)
}
以上办法都是利用同步机制来防止同一时刻读写数据的问题的发生。使读写操作满足happened-before原则。
Vector clock
Go race 检测原理
Go 语言内置的race检测工具为Google的Threadsanitizer,其内部原理即为Vector clock(矢量时钟)。Go语言在其源码中插入了大量的race相关代码,这些探针在需要race检测时被触发,同时,需要race检测时,编译器还会向可能发生data race的用户代码中插入探针。这些探针维护着矢量时钟的状态,从而感知到race的发生。由于插入探针,所以需要race检测的代码运行效率会更低,且内内存占用会更大。
Reference
https://en.wikipedia.org/wiki/Vector_clock
《Go 语言底层原理剖析》