Block Rockin’ Codes

back with another one of those block rockin' codes

Go1.1 の Race Detector

intro

先々週、Go 1.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 に書き換えるというような癖をつけると、幸せになれるのかもしれません。