乐趣区

关于golang:使用RaceDetector为并发代码保驾护航

明天咱们来聊聊 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)

退出移动版