Go 数据竞争
# 一、数据竞争的检测
数据竞争(data race)是两个或多个 goroutine 访问同一个资源(如变量或数据结构),并尝试对该资源进行读写而不考虑其他 goroutine。这种类型的代码会出现非常随机的 bug,通常需要大量的日志记录和运气才能找到这种类型的 bug。
那我们有没有什么办法检测它呢?Go 官方早在 1.1 版本就引入了数据竞争的检测工具,我们只需要在执行测试或者是编译的时候加上 -race
的 flag 就可以开启数据竞争的检测:
go test -race ./...
go build -race
2
注意
- 不建议在生产环境 build 的时候开启数据竞争检测,因为这会带来一定的性能损失(一般内存 5 ~ 10 倍,执行时间 2 ~ 20 倍),当然,必须要 debug 的时候除外;
- 建议在执行单元测试时始终开启数据竞争的检测。
# 二、竞争检测工具的使用
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var counter int
func main() {
for i := 0; i < 1000; i++ {
wg.Add(1)
go routine()
}
wg.Wait()
fmt.Printf("Final Counter: %d\n", counter)
}
func routine() {
value := counter
value++
counter = value
wg.Done()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
上面这段代码其中三次的执行结果分别是:
Final Counter: 947
Final Counter: 961
Final Counter: 969
为什么会导致这样的结果呢,是因为每一次 for 循环中,我们都使用 go routine()
启动了一个 goroutine,它们都对同一个变量进行了读写操作,但是我们并没有控制它的执行顺序,因此每次执行结果都不同。
可以发现,写出这种代码时上线后如果出现 bug 会非常难定位,因为你不知道到底是哪里出现了问题,所以我们就要在测试阶段就结合 data race 工具提前发现问题。
我们执行以下命令:
go run -race ./main.go
运行结果如下:
==================
WARNING: DATA RACE
Read at 0x000001232c90 by goroutine 8:
main.routine()
/Users/lichangao/Study/Go/go_learning/src/main/main.go:21 +0x3e
Previous write at 0x000001232c90 by goroutine 7:
main.routine()
/Users/lichangao/Study/Go/go_learning/src/main/main.go:23 +0x5a
Goroutine 8 (running) created at:
main.main()
/Users/lichangao/Study/Go/go_learning/src/main/main.go:14 +0x68
Goroutine 7 (finished) created at:
main.main()
/Users/lichangao/Study/Go/go_learning/src/main/main.go:14 +0x68
==================
Final Counter: 999
Found 1 data race(s)
exit status 66
这个结果非常清晰的告诉了我们在 21 行这个地方我们有一个 goroutine 在读取数据,但是呢,在 23 行这个地方又有一个 goroutine 在写入,所以产生了数据竞争。
然后下面分别说明这两个 goroutine 是什么时候创建的,以及当前是否在运行当中。
# 三、典型案例
# 3.1 案例一:在循环中启动 goroutine 引用临时变量
func main() {
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()
}()
}
wg.Wait()
}
2
3
4
5
6
7
8
9
10
11
这段代码最有可能会输出 5 个 5,因为在 for 循环的 i++ 会执行的快一些,所以在最后打印的结果都是 5。
但是这里是有数据竞争的,在新启动的 goroutine 当中读取 i 的值,在 main 中写入,导致出现了 data race,这个结果应该是不可预知的,因为我们不能假定 goroutine 中 print 就一定比外面的 i++ 慢,习惯性的做这种假设在并发编程中是很有可能会出问题的。
func main() {
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()
}
2
3
4
5
6
7
8
9
10
11
这个要修改也很简单,只需要将 i 作为参数传入即可,这样每个 goroutine 拿到的都是拷贝后的数据。
# 3.2 案例二:意外共享变量
package main
import (
"os"
)
func main() {
ParallelWrite([]byte("xxx"))
}
// ParallelWrite writes data to file1 and file2, returns the errors.
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,
// 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.
if err != nil {
res <- err
} else {
go func() {
_, err = f2.Write(data)
res <- err
f2.Close()
}()
}
return res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
我们使用 go run -race ./main.go
执行,可以发现这里报错的地方是,19 行和 24 行,有 data race,这里是因为共享了 err 这个变量。
==================
WARNING: DATA RACE
Write at 0x00c00009c1e0 by main goroutine:
main.ParallelWrite()
/Users/lichangao/Study/Go/go_learning/src/main/main.go:24 +0x1dd
main.main()
/Users/lichangao/Study/Go/go_learning/src/main/main.go:6 +0x84
Previous write at 0x00c00009c1e0 by goroutine 7:
main.ParallelWrite.func1()
/Users/lichangao/Study/Go/go_learning/src/main/main.go:19 +0x94
Goroutine 7 (finished) created at:
main.ParallelWrite()
/Users/lichangao/Study/Go/go_learning/src/main/main.go:16 +0x336
main.main()
/Users/lichangao/Study/Go/go_learning/src/main/main.go:6 +0x84
==================
修改的话只需要在两个 goroutine 中使用新的临时变量就行了:
...
_, err := f1.Write(data)
...
_, err := f1.Write(data)
...
2
3
4
5
注意
Go 中初始化变量的时候如果在同一个作用域下,如下方代码,这里使用的 err 其实是同一个变量,只是 f1 和 f2 不同,具体可以看 Effective Go: Redeclaration and reassignment (opens new window) 的内容。
f1, err := os.Create("a")
f2, err := os.Create("b")
2
# 3.3 案例三:未受保护的全局变量
如果有多个 goroutine 调用下面这段代码,会导致 service map 的数据竞争。
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]
}
2
3
4
5
6
7
8
9
要使这段代码安全运行,可以通过 mutex 来控制访问:
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]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 3.4 案例四:未受保护的基本类型变量
数据竞争也可能发生在基本类型的变量(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)
}
}
}()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这里可以用 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)
}
}
}()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3.5 案例五:非同步的发送和关闭操作
同一 channel 上的非同步发送和关闭操作也可能是竞争条件:
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)
2
3
4
5
6
7
根据 Go 的内存模型,一个 channel 上的发送会在该 channel 的相应接收完成之前发生。为了同步发送和关闭操作,可以使用一个接收操作,保证在关闭之前完成发送。
c := make(chan struct{}) // or buffered channel
go func() { c <- struct{}{} }()
<-c
close(c)
2
3
4
5
# 3.6 案例六:没有安全的 data race
package main
import "fmt"
type IceCreamMaker interface {
// Hello greets a customer
Hello()
}
type Ben struct {
name string
}
func (b *Ben) Hello() {
fmt.Printf("Ben says, \"Hello my name is %s\"\n", b.name)
}
type Jerry struct {
name string
}
func (j *Jerry) Hello() {
fmt.Printf("Jerry says, \"Hello my name is %s\"\n", j.name)
}
func main() {
var ben = &Ben{"Ben"}
var jerry = &Jerry{"Jerry"}
var maker IceCreamMaker = ben
var loop0, loop1 func()
loop0 = func() {
maker = ben
go loop1()
}
loop1 = func() {
maker = jerry
go loop0()
}
go loop0()
for {
maker.Hello()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
以上代码最后输出的结果可能有以下情况:
...
Ben says, "Hello my name is Jerry"
...
Jerry says, "Hello my name is Ben"
...
这是因为我们在对 maker 赋值的时候并不是原子的,在 Go 内存模型:Happens Before - 机器字 中我们讲到过,只有对单个机器字进行赋值的时候才是原子的,虽然这个看上去只有一行,但是 interface 在 Go 中其实是一个结构体,它包含了 type 和 data 两个部分,如下定义,所以它的赋值也不是原子的,会出现问题。
type interface struct {
Type uintptr // points to the type of the interface implementation
Data uintptr // holds the data for the interface's receiver
}
2
3
4
这个案例的可怕点在于,这两个结构体的内存布局一模一样,所以出现错误也不会 panic 退出,如果在其中一个结构体里面再加入一个 string 的字段,去读取就会导致 panic,这种错误在线上实在太难发现了,而且很有可能会很致命。因此,不要假设安全的 data race。