Go1.1 の Race Detector
Race Detector
この機能は、簡単に言うと「レースコンディションが発生していないか」を調べる機能です。
といわれると、なんだかすごい機能ですね。
そもそもレースコンディションとは、マルチスレッドプログラミングなどで、単一のリソースを複数のスレッドで共有した際に、競合状態が発生して、予期しない結果を生んだりする状態です。
レースコンディションによるバグは、再現生が低かったりするので、一般的にデバッグが難しいとされています。
そうした状態が起こらないように、がっちりロックを取り合ったり、そもそもメモリを共有せずメッセージパッシングするなど、別のパラダイムで情報を共有する方法が取られます。
Go も、以下のように「メモリ共有より、メッセージ共有」という方針を推奨してます。
"Do not communicate by sharing memory; instead, share memory by communicating"
そこで使われるのが Goroutine と channel で、それについては Go の並行処理 - Block Rockin’ Codes でも書きました。
で、今回新しく入った機能は、そのレースコンディションが発生していないかどうかを、コンパイル時に Race Detector を仕込んでおくことで、実行時に調べることができるようです。
やっぱり、なんだかすごい機能ですね。
レースコンディションの例
Go の場合、複数の Goroutine から同一のメモリを変更するような場面がそれにあたります。
一番単純なのは、以下のような例でしょうか。
package main import ( "fmt" "time" ) func main() { num := 10 go func() { num += 1 fmt.Println(num) }() num += 1 fmt.Println(num) time.Sleep(time.Second) }
main 自体も Goroutine なので、 num は main の Goroutine と、その中の匿名関数の Goroutine との間で共有されていて、両方が変更と出力を行なっています。
普通に実行すると、以下のとおりです。
$ go run test.go 11 12
まあ、そうでしょう。
-race オプション
では race detection してみます。
実行やビルドのコマンドに -race オプションを足すだけです。
$ go test -race パッケージ $ go run -race ファイル $ go build -race パッケージ $ go install -race パッケージ
先ほどのを実行してみると。
$ go run -race test.go 11 ================== WARNING: DATA RACE Write by goroutine 4: main.func·001() /private/tmp/race/test.go:11 +0x37 gosched0() /usr/local/go/src/pkg/runtime/proc.c:1218 +0x9f Previous write by goroutine 1: main.main() /private/tmp/race/test.go:14 +0xb5 runtime.main() /usr/local/go/src/pkg/runtime/proc.c:182 +0x91 Goroutine 4 (running) created at: main.main() /private/tmp/race/test.go:13 +0xa5 runtime.main() /usr/local/go/src/pkg/runtime/proc.c:182 +0x91 Goroutine 1 (running) created at: _rt0_amd64() /usr/local/go/src/pkg/runtime/asm_amd64.s:87 +0x106 ================== 12 Found 1 data race(s) exit status 66
標準出力には結果、エラー出力にこのトレースログがでます。(上は一緒にしてます。)
ざっくりみてみると、
- 13 行目で生成された Goroutine 4 が 11 行目で
- main メソッドである Goroutine 1 が 14 行目で
それぞれ同じ領域に書き込みしてるというように読み取れます。
解消方法は色々ありますが、とりあえず以下だとレースコンディション自体はなくなります。
func main() { num := 10 go func(num int) { num += 1 fmt.Println(num) }(num) num += 1 fmt.Println(num) time.Sleep(time.Second) }
$ go run -race test.go 11 11
ログは出ません。
どうやら、 Goroutine ごとのメモリアクセスの履歴をもとに、競合が起こってないかをしらべているようです。よくできてますねぇ。すごい。
オプション
GORACE という環境変数を通じてオプションを渡せます。
- log_path (default stderr): ログファイルのパス(ファイル名は path.pid)。デフォルトは stderr
- exitcode (default 66): race があった時終わらせるときのステータスコード
- strip_path_prefix (default ""): 指定したパスをログから消せる
- history_size (default 1): Goroutine のメモリアクセス履歴を 32K * 2**history_size で調節
こんな感じ
$ GORACE="log_path=/tmp/race history_size=4" go run -race test.go
注意点
現在は darwin/amd64, linux/amd64, and windows/amd64 のみの対応。
実行時のメモリ使用量が 5-10倍, 実行時間が 2-20倍に 増えるようです。
テストで確認しておいて、ビルド時は外すとかの方がよさそうですね。
まとめ
メッセージパッシングに寄せて Goroutine + Channel でやっていたとしても、レースコンディションが起こってしまうこともあるかもしれません。
そして、レースコンディションは、「起こってない」ことを調べるのは一般的には難しいと思うんですが、その検出を言語のコアに取り込んでいるというのが Go らしくていいと思います。
テストでは必ずつけて、みつけたら channel に書き換えるというような癖をつけると、幸せになれるのかもしれません。