CA Reward

Tech Blog

Tech Blogトップに戻る

GoのRaceDetectorと気をつけるべき所

2016.12.02

  • このエントリーをはてなブックマークに追加
  • Pocket
daich
daichエンジニア

この記事はGo (その3) Advent Calendar 2016 2日目の記事です。


こんにちは。開発部の平田です。今回は、Golang に標準で組み込まれているデータ競合の検知の仕組みである race detector と、どのようなケースで検知してくれるのかについて https://golang.org/doc/articles/race_detector.html を元に簡単に紹介したいと思います。

Golang には goroutine という並行処理のための機構があるので、気軽に goroutine を起動して並行処理を書くことが出来ます。その為、意図しない所でレースコンディションが発生してしまうことが時々あります。極力そういったことが起きないように基本的には channel を使って値をコピーして渡したりするのですが、思わぬ所で発生してしまうデータレースに対して検知する仕組みが標準で組み込まれています。

使い方

使い方は、各種コマンドに -race flag を追加します。

データレースが見つかった場合には、以下のように出力されます。

また、幾つかのオプションを環境変数 GORACE 経由で渡すことが出来ます。

  • log_path (default stderr)
  • exitcode (default 66)
  • strip_path_prefix (default "")
  • 指定したパスのログを取り除く
  • history_size (default 1)
  • goroutine のメモリアクセス履歴。32K * 2**history_size
  • halt_on_error (default 0)
  • データレースが発見した段階でプログラムを止める

例えばデータレースが見つかった段階で exit code 1 で終了させたい場合には

といったように指定します。

Data Race

具体的に検知されるデータレースにはどのようなものがあるかを簡単に紹介します。

Mapへの同時書き込み

この例では、m を main と goroutine の両方から排他制御を行わずに書き込んでいる為、データレースの警告が表示されます。(このケースでは検出されませんでしたが、go 1.6 からは map への書き込み中に別の goroutine からアクセスすると panic します)

これを対応するためには、書き込みの前後を排他的に制御する必要があります。実際にこの様に対応するとは無いかもしれませんが、今回は素直に前後にロックを取得するようにします。

これで、race flag 付きの実行でも警告が出なくなります。注意する点として、map は実体への参照を保持する構造体なので、関数へ値渡しするだけでは対応できません。

ループカウンター

i がループカウンタ用の変数として定義されていますが、その値を goroutine から読み込んでいます。本来であれば、 01234 という出力を期待していますが、実際に実行してみるとそのようにはならないことがわかると思います。

今回のケースでは、i が int で primitive な型なので、関数の引数として値コピーしてあげることで対応できます。

グローバルな変数への書き込み

この場合も最初の例と同様に、map の前後を排他的に制御する必要があります。

map 以外のデータ型の場合でも同様に、排他制御を行う必要があります。

Primitive な値の読み込み

これまで map を例にサンプルを見てきましたが、primitive な型(bool・int・unit32・etc...)でも同様の問題は発生します。

このケースの場合、c.last の読み込みと、KeepAliveによる c.last への書き込みが競合してしまいます。 last は型が int64 なので一見問題無さそうにも思えますが、それは Golang の言語仕様としてメモリへのアクセスがアトミックというわけでは無く、x86-64 64bit の mov 命令がアトミックだからです。その他の環境では正しく動かない事があります。また、排他制御を行わない場合、コンパイラは対象 goroutine の中だけで破綻が起きないように最適化を行えるので、リオーダーなどで複数 goroutine 間でデバッグが非常に難しい問題が発生する場合があります。(これはC++などの言語でも同じです)

このケースでも、mutex か channel で対応することが出来ますが、int の場合には sync/atomic パッケージでアトミックな操作が行えるので、今回はこちらを使って対応します。

atomic パッケージの各種関数は、上で説明したように各アーキテクチャ毎のメモリモデルや環境に合わせて実行すべき命令の差を吸収してくれています。(例えば x86 環境での int64 へのアトミックな操作など)

似たようなケースで、例えば bool によってフラグを管理しているケースなどでも応用できます。

goroutine safe ではない bool を0/1で代用することによって、atomic に操作できます。

まとめ

上記以外にも、データレースは様々なケースで発生します。どうしてもパフォーマンスがほしい所以外では基本的には channel を使って通信によるメモリの共有を行うのが一番良いとは思いますが、想定外の所で発生してしまうケースはあると思います。レースコンディションを起こさないように正しくプログラミング・レビューを行うことはとても難しいので、こういった仕組みが公式に用意されているのはとても助かります。

もしまだ導入していないのであれば、テストの実行に是非検討してみては如何でしょうか。

daich
daichエンジニア