明天咱们来聊聊 Golang 中剖析并发 Bug 的神器:race detector。咱们会先理解数据竞争,以及原子性等概念,进而通过理论的例子来领会和应用 race detector。文末还会剖析 dave 大神 的博客上一个典型的与 data race 无关的案例。
愿咱们的代码永不呈现 并发 Bug~
什么是 data race?
当多个 goroutine 并发地拜访同一个变量,并且至多有一个拜访是写时,就会产生 data race
(数据竞争)
由data race
引起的 Bug 特地难以剖析,既因为它出 Bug 比拟“随机”,也因为并发程序人造的高复杂度。
原子赋值
产生 data race
的重要起因是,程序的很多操作不是原子的。(原子性就是 CPU 一口气干完,不存在一个中间状态)
在 Go(甚至是大部分语言)中,一条一般的赋值语句其实并不是一个原子操作。
例如,在 32 位机器上写 int64
类型的变量是有中间状态的,它会被拆成两次写操作 MOV
—— 写低 32 位和写高 32 位(也就是所谓的撕写),如下图所示:
命令:
go tool compile -S xx.go
如果一个线程刚写完低 32 位,还没来得及写高 32 位时,另一个线程读取了这个变量,那它失去的就是一个毫无逻辑的两头变量,这很有可能使咱们的程序呈现诡异的 Bug。
对于 64 位的机器,一个指针的大小是 8 bytes(字节),一个 8 个字节的赋值是原子的。
race detector
在 Golang1.1 版本中,Golang 引入了race detector
。它能检测并报告它发现的任何 data race。咱们只须要在执行测试或者是编译的时候加上 -race
的 flag 就能够开启数据竞争的检测
go build -race
对性能有影响,除非生产环境出了很难排查的并发 BUG,否则不倡议这么做。-
go test -race
配置
能够应用
GORACE 环境变量
设置比赛检测器。格局是:GORACE="option1=val1 option2=val2"
珍藏我的文章,须要查这个表的时候记得回来看看~
log_path | 其报告写入一个名为 log_path.pid 的文件。默认写入 stderr |
exitcode | 状态码,默认值为 66 |
strip_path_prefix | 去掉日志前缀,默认值为空串 |
history_size (default 1) | 每个 goroutine 的内存拜访历史记录是 32K 2*history_size 元素。减少这个值能够防止报告中的“复原堆栈失败”谬误,但代价是减少内存使用量。 |
halt_on_error | 控制程序是否在报告第一次数据竞争后退出,默认不开启 |
atexit_sleep_ms | 退出主 goroutine 之前休眠的毫秒数,默认 1000 毫秒 |
举例:$ GORACE="log_path=/tmp/race/report strip_path_prefix=/my/go/sources/" go test -race
用 race detector 检测数据竞争
读写同一个循环变量
咱们用一个简略的例子来领会数据竞争,以及如何应用 race detector
来检测它:
// 循环创立 goroutine 打印 i 变量,goroutine 应用长期变量会产生 data race
func TestDataRaceForRangeAdd(t *testing.T) {
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()}()
// 应用 time.sleep 后,因为 for 循环变慢了,程序失常打印出 0 1 2 3 4
//time.Sleep(time.Second)
}
wg.Wait()}
---------------------- 运行后果 -----------------------
5 5 5 5 5
咱们应用 race detector
来查看:日志打印出了堆栈信息,以及所波及的 goroutines 被创立的堆栈。
==================
WARNING: DATA RACE
Read at 0x00c0000a6078 by goroutine 8:
command-line-arguments.TestDataRaceForRangeAdd.func1()
/Users/haoyufei/code/GOLang-Lab/detect_race/detect_race_demo3/detect_race_test.go:14 +0x3c
Previous write at 0x00c0000a6078 by goroutine 7:
command-line-arguments.TestDataRaceForRangeAdd()
/Users/haoyufei/code/GOLang-Lab/detect_race/detect_race_demo3/detect_race_test.go:12 +0x104
testing.tRunner()
/usr/local/go/src/testing/testing.go:1193 +0x202
Goroutine 8 (running) created at:
command-line-arguments.TestDataRaceForRangeAdd()
/Users/haoyufei/code/GOLang-Lab/detect_race/detect_race_demo3/detect_race_test.go:13 +0xdc
testing.tRunner()
/usr/local/go/src/testing/testing.go:1193 +0x202
Goroutine 7 (running) created at:
testing.(*T).Run()
/usr/local/go/src/testing/testing.go:1238 +0x5d7
testing.runTests.func1()
/usr/local/go/src/testing/testing.go:1511 +0xa6
testing.tRunner()
/usr/local/go/src/testing/testing.go:1193 +0x202
testing.runTests()
/usr/local/go/src/testing/testing.go:1509 +0x612
testing.(*M).Run()
/usr/local/go/src/testing/testing.go:1417 +0x3b3
main.main()
_testmain.go:43 +0x236
==================
不小心被共享的 error
// 不小心在多个 goroutine 共享了 error 变量
func TestDataRaceError(t *testing.T) {data := []byte("data")
res := make(chan error, 2)
f1, err := os.Create("file1")
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.
go func() {_, err = f2.Write(data)
res <- err
f2.Close()}()}
==================
WARNING: DATA RACE
Write at 0x00c00011a4e0 by goroutine 8:
command-line-arguments.TestDataRaceError.func1()
/Users/haoyufei/code/GOLang-Lab/detect_race/detect_race_demo3/detect_race_test.go:33 +0x94
Previous write at 0x00c00011a4e0 by goroutine 7:
command-line-arguments.TestDataRaceError()
/Users/haoyufei/code/GOLang-Lab/detect_race/detect_race_demo3/detect_race_test.go:38 +0x1e9
testing.tRunner()
/usr/local/go/src/testing/testing.go:1193 +0x202
Goroutine 8 (running) created at:
command-line-arguments.TestDataRaceError()
/Users/haoyufei/code/GOLang-Lab/detect_race/detect_race_demo3/detect_race_test.go:30 +0x190
testing.tRunner()
/usr/local/go/src/testing/testing.go:1193 +0x202
Goroutine 7 (running) created at:
testing.(*T).Run()
/usr/local/go/src/testing/testing.go:1238 +0x5d7
testing.runTests.func1()
/usr/local/go/src/testing/testing.go:1511 +0xa6
testing.tRunner()
/usr/local/go/src/testing/testing.go:1193 +0x202
testing.runTests()
/usr/local/go/src/testing/testing.go:1509 +0x612
testing.(*M).Run()
/usr/local/go/src/testing/testing.go:1417 +0x3b3
main.main()
_testmain.go:45 +0x236
==================
--- FAIL: TestDataRaceError (0.00s)
经典的案例
咱们来看一个乏味的例子:开两个 goroutine 并发的对 User 接口赋值
会产生什么
内容来自 dave 大神的一篇博客
// 首先定义一个 User 接口和它的两个实现类 Tom & Jerry
type User interface {Hello()
}
type Tom struct {name string}
func (s *Tom) Hello() {log.Printf("Tom say: I'm %s", s.name)
}
type Jerry struct {
id int
name string
}
func (s *Jerry) Hello() {log.Printf("Jerry say: I'm %s", s.name)
}
// 而后开两个 goroutine 并发的对 User 接口赋值
func Test(t *testing.T) {tom := &Tom{"Tom"}
jerry := &Jerry{id: 1, name: "Jerry"}
var u User = tom
var loopA, loopB func()
loopA = func() {
u = tom
go loopB()}
loopB = func() {
u = jerry
go loopA()}
go loopA()
for {u.Hello()
}
}
------------------ 运行后果 --------------------
--- FAIL: Test (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
这是为什么呢?咱们来看看 Golang 中对 interface 的定义
type interface struct {
Type uintptr // 指向类型
Data uintptr // 指向数据
}
在对接口 User 赋值时,必须更新接口的两个属性,这意味着 对接口赋值不是原子的。
这时咱们将 Jerry 构造体的字段改的跟 Tom 构造体一样,会产生“神奇的事”:
type User interface {Hello()
}
type Tom struct {name string}
func (s *Tom) Hello() {log.Printf("Tom say: I'm %s", s.name)
}
type Jerry struct {name string}
func (s *Jerry) Hello() {log.Printf("Jerry say: I'm %s", s.name)
}
---------------------- 运行后果 ------------------------
Tom says, "Hello my name is Tom"
Jerry says, "Hello my name is Jerry"
Jerry says, "Hello my name is Jerry"
Tom says, "Hello my name is Jerry"
Tom says, "Hello my name is Tom"
再跑一次测试,发现没有 nil pointer panic
了。这是为什么呢?因为 Tom 构造体 和 Jerry 构造体是内存对齐的。
然而!!!呈现了一个乏味的景象:Tom says, "Hello my name is Jerry"
,这意味着指向 Tom 类型的指针,调用了 Jerry 的数据。
这是因为对接口赋值不是原子的,有可能呈现 Type 字段改了,然而 Data 字段还没改
的状况
比方上文中,Type 指向了 Jerry 类型,而 Data 指向了 Tom。
试想如果咱们上线了这样的代码,其排查难度有多大
幸好咱们能够应用 GORACE="halt_on_error=1" go test -race detect_race_test.go
命令检测出这个 Bug:
==================
WARNING: DATA RACE
Write at 0x00c00011a4c0 by 2022/04/15 21:12:03 Tom say: I'm Tom
goroutine 8:
command-line-arguments.Test.func1()
/Users/haoyufei/code/GOLang-Lab/detect_race/detect_race_demo2/detect_race_test.go:47 +0x5c
Previous read at 0x00c00011a4c0 by goroutine 7:
command-line-arguments.Test()
/Users/haoyufei/code/GOLang-Lab/detect_race/detect_race_demo2/detect_race_test.go:59 +0x308
testing.tRunner()
/usr/local/go/src/testing/testing.go:1193 +0x202
Goroutine 8 (running) created at:
command-line-arguments.Test()
/Users/haoyufei/code/GOLang-Lab/detect_race/detect_race_demo2/detect_race_test.go:56 +0x2fa
testing.tRunner()
/usr/local/go/src/testing/testing.go:1193 +0x202
Goroutine 7 (running) created at:
testing.(*T).Run()
/usr/local/go/src/testing/testing.go:1238 +0x5d7
testing.runTests.func1()
/usr/local/go/src/testing/testing.go:1511 +0xa6
testing.tRunner()
/usr/local/go/src/testing/testing.go:1193 +0x202
testing.runTests()
/usr/local/go/src/testing/testing.go:1509 +0x612
testing.(*M).Run()
/usr/local/go/src/testing/testing.go:1417 +0x3b3
main.main()
_testmain.go:43 +0x236
==================
创作不易,心愿大家能棘手点个赞~这对我很重要,蟹蟹各位啦~
参考文献
[https://go.dev/doc/articles/race_detector](https://go.dev/doc/articles/race_detector)
[https://dave.cheney.net/2014/06/27/ice-cream-makers-and-data-races](https://dave.cheney.net/2014/06/27/ice-cream-makers-and-data-races)