明天咱们来聊聊 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 racefunc 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 RACERead 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 +0x3cPrevious 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 +0x202Goroutine 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 +0x202Goroutine 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 RACEWrite 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 +0x94Previous 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 +0x202Goroutine 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 +0x202Goroutine 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 & Jerrytype 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 RACEWrite at 0x00c00011a4c0 by 2022/04/15 21:12:03 Tom say: I'm Tomgoroutine 8:  command-line-arguments.Test.func1()      /Users/haoyufei/code/GOLang-Lab/detect_race/detect_race_demo2/detect_race_test.go:47 +0x5cPrevious 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 +0x202Goroutine 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 +0x202Goroutine 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)