使用RaceDetector为并发代码保驾护航

今天我们来聊聊 Golang 中分析并发 Bug 的神器:race detector。我们会先了解数据竞争,以及原子性等概念,进而通过实际的例子来体会和使用 race detector。文末还会分析 dave大神 的博客上一个典型的与 data race 有关的案例。
愿我们的代码永不出现 并发Bug~
使用RaceDetector为并发代码保驾护航_第1张图片

什么是 data race?

当多个goroutine并发地访问同一个变量,并且至少有一个访问是写时,就会发生data race(数据竞争)
data race引起的Bug特别难以分析,既因为它出Bug比较“随机”,也因为并发程序天然的高复杂度。

原子赋值

产生data race的重要原因是,程序的很多操作不是原子的。(原子性就是 CPU 一口气干完,不存在一个中间状态)
在 Go(甚至是大部分语言)中,一条普通的赋值语句其实并不是一个原子操作。
例如,在 32 位机器上写 int64 类型的变量是有中间状态的,它会被拆成两次写操作 MOV—— 写低 32 位和写高 32 位(也就是所谓的撕写),如下图所示:

命令: go tool compile -S xx.go

使用RaceDetector为并发代码保驾护航_第2张图片
如果一个线程刚写完低 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。

使用RaceDetector为并发代码保驾护航_第3张图片

试想如果我们上线了这样的代码,其排查难度有多大
使用RaceDetector为并发代码保驾护航_第4张图片
幸好我们可以使用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)

你可能感兴趣的:(使用RaceDetector为并发代码保驾护航)