Carpe Diem

備忘録

GoのRace Detectorでマルチスレッドでのデータ競合検知

概要

Go1.6からマップへの同時アクセスを検知する機能が追加されました。
しかしそれ以前からrace detectorが標準で備わっているため、複数のスレッドによる同時アクセスで起こるデータ競合の検知はできます。
今回はその使い方を紹介します。

環境

  • Go 1.6

使い方

-raceオプションを付けます。

$ go run -race xxx.go

$ go test -race mypkg

のように実行すればOKです。

サンプル

例えば以下の様なコードがあったとします。

package main

import "fmt"

func main() {
    m := make(map[int]int)
    go func() {
        for i := 0; i < 10; i++ {
            m[i] = i + 1
        }
    }()
    for i := 0; i < 10; i++ {
        fmt.Printf("%d\n", m[i])
    }
}

これに対して通常の実行をすると

$ go run test.go
0
0
0
0
0
0
0
0
0
0

となり、一見問題無さそうに見えますが、race detectorを使うと

$ go run -race test.go 
0
0
0
0
0
0
==================
WARNING: DATA RACE0

Write by goroutine 6:
0
  runtime.mapassign1()
      /usr/local/opt/go/libexec/src/runtime/hashmap.go:429 +0x0
  main.main.func1()
      /Users/xxxxx/test.go:9 +0x6b

0
Previous read by main goroutine:
  runtime.mapaccess1_fast64()
      /usr/local/opt/go/libexec/src/runtime/hashmap_fast.go:103 +0x0
0
  main.main()
      /Users/xxxxx/test.go:13 +0xb9

Goroutine 6 (running) created at:
  main.main()
      /Users/xxxxx/test.go:11 +0x89
==================
Found 1 data race(s)
exit status 66

こんな感じで問題を指摘してくれます。

調べ方

ポイントは

  • Write by goroutine n:
  • Previous read by main goroutine:

  • Read by goroutine n:
  • Previous write by goroutine m:

のように何かしらの変数がgoroutineによってreadとwriteが同時に行われた箇所を見つけることです。
今回はmapですが、例えばglobal変数をテストで上書きしてたりとかしたなど、様々なケースで起きます。

Go 1.6の検知は?

先ほどのコードですが、ループ回数を増やすとGo自体も検知してくれます。

$ go run test.go 
0
0
0
0
0
0
~~~~~~
0
0
0
0
0
0
0
0
fatal error: concurrent map read and map write

goroutine 1 [running]:
runtime.throw(0x1182e0, 0x21)
    /usr/local/opt/go/libexec/src/runtime/panic.go:530 +0x90 fp=0xc82003fe98 sp=0xc82003fe80
runtime.mapaccess1_fast64(0xb78a0, 0xc8200121b0, 0x9a, 0x1)
    /usr/local/opt/go/libexec/src/runtime/hashmap_fast.go:112 +0x5a fp=0xc82003feb8 sp=0xc82003fe98
main.main()
    /Users/xxxxx/test.go:13 +0xab fp=0xc82003ff50 sp=0xc82003feb8
runtime.main()
    /usr/local/opt/go/libexec/src/runtime/proc.go:188 +0x2b0 fp=0xc82003ffa0 sp=0xc82003ff50
runtime.goexit()
    /usr/local/opt/go/libexec/src/runtime/asm_amd64.s:1998 +0x1 fp=0xc82003ffa8 sp=0xc82003ffa0

goroutine 5 [runnable]:
main.main.func1(0xc8200121b0)
    /Users/xxxxx/test.go:9 +0x60
created by main.main
    /Users/xxxxx/test.go:11 +0x79
exit status 2

なぜ先ほどは検知できなかったのかと疑問に思うと思います。
というのもこのチェックはBest Effortでの検知なので、必ず検知するわけではなく「もしかしたら検知してくれる」くらいに考えておいた方が良いです。

ソース