データレース検出器
はじめに
データレースは、並行システムにおける最も一般的でデバッグが難しいバグの1つです。データレースは、2つのゴルーチンが同じ変数に同時にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生します。詳細については、Goメモリモデルを参照してください。
クラッシュやメモリ破損につながる可能性のあるデータレースの例を次に示します。
func main() { c := make(chan bool) m := make(map[string]string) go func() { m["1"] = "a" // First conflicting access. c <- true }() m["2"] = "b" // Second conflicting access. <-c for k, v := range m { fmt.Println(k, v) } }
使い方
このようなバグの診断を支援するために、Goには組み込みのデータレース検出器が含まれています。これを使用するには、goコマンドに-race
フラグを追加します。
$ go test -race mypkg // to test the package $ go run -race mysrc.go // to run the source file $ go build -race mycmd // to build the command $ go install -race mypkg // to install the package
レポート形式
レース検出器がプログラム内でデータレースを検出すると、レポートが出力されます。レポートには、競合するアクセスに関するスタックトレースと、関係するゴルーチンが作成されたスタックが含まれます。以下に例を示します。
WARNING: DATA RACE Read by goroutine 185: net.(*pollServer).AddFD() src/net/fd_unix.go:89 +0x398 net.(*pollServer).WaitWrite() src/net/fd_unix.go:247 +0x45 net.(*netFD).Write() src/net/fd_unix.go:540 +0x4d4 net.(*conn).Write() src/net/net.go:129 +0x101 net.func·060() src/net/timeout_test.go:603 +0xaf Previous write by goroutine 184: net.setWriteDeadline() src/net/sockopt_posix.go:135 +0xdf net.setDeadline() src/net/sockopt_posix.go:144 +0x9c net.(*conn).SetDeadline() src/net/net.go:161 +0xe3 net.func·061() src/net/timeout_test.go:616 +0x3ed Goroutine 185 (running) created at: net.func·061() src/net/timeout_test.go:609 +0x288 Goroutine 184 (running) created at: net.TestProlongTimeout() src/net/timeout_test.go:618 +0x298 testing.tRunner() src/testing/testing.go:301 +0xe8
オプション
GORACE
環境変数は、レース検出器のオプションを設定します。形式は次のとおりです。
GORACE="option1=val1 option2=val2"
オプションは次のとおりです。
-
log_path
(デフォルトstderr
):レース検出器は、レポートをlog_path.pid
という名前のファイルに書き込みます。特別な名前stdout
とstderr
を使用すると、レポートはそれぞれ標準出力と標準エラーに書き込まれます。 -
exitcode
(デフォルト66
):検出されたレース後に終了する際に使用する終了ステータス。 -
strip_path_prefix
(デフォルト""
):レポートをより簡潔にするために、レポートされたすべてのファイルパスからこのプレフィックスを削除します。 -
history_size
(デフォルト1
):ゴルーチンごとのメモリアクセス履歴は、32K * 2**history_size 要素
です。この値を大きくすると、レポートで「スタックの復元に失敗しました」というエラーを回避できますが、メモリ使用量が増加します。 -
halt_on_error
(デフォルト0
):最初のデータレースを報告した後にプログラムを終了するかどうかを制御します。 -
atexit_sleep_ms
(デフォルト1000
):終了する前にメインゴルーチンでスリープするミリ秒数。
例
$ GORACE="log_path=/tmp/race/report strip_path_prefix=/my/go/sources/" go test -race
テストの除外
-race
フラグを使用してビルドすると、go
コマンドは追加のビルドタグ race
を定義します。このタグを使用して、レース検出器を実行するときに一部のコードとテストを除外できます。いくつかの例を挙げます。
// +build !race package foo // The test contains a data race. See issue 123. func TestFoo(t *testing.T) { // ... } // The test fails under the race detector due to timeouts. func TestBar(t *testing.T) { // ... } // The test takes too long under the race detector. func TestBaz(t *testing.T) { // ... }
使用方法
まず、レース検出器を使用してテストを実行します(go test -race
)。レース検出器は、実行時に発生するレースのみを検出するため、実行されないコードパスのレースを検出できません。テストの範囲が不完全な場合、-race
でビルドされたバイナリを現実的なワークロードで実行することで、さらに多くのレースが見つかる可能性があります。
典型的なデータレース
以下に、典型的なデータレースをいくつか示します。これらはすべて、レース検出器で検出できます。
ループカウンタでのレース
func main() { var wg sync.WaitGroup wg.Add(5) var i int for i = 0; i < 5; i++ { go func() { fmt.Println(i) // Not the 'i' you are looking for. wg.Done() }() } wg.Wait() }
関数リテラルの変数i
は、ループで使用されるのと同じ変数であるため、ゴルーチンでの読み取りはループのインクリメントと競合します。(このプログラムは通常、01234ではなく55555を出力します。)このプログラムは、変数のコピーを作成することで修正できます。
func main() { var wg sync.WaitGroup wg.Add(5) var i int for i = 0; i < 5; i++ { go func(j int) { fmt.Println(j) // Good. Read local copy of the loop counter. wg.Done() }(i) } wg.Wait() }
誤って共有された変数
// ParallelWrite writes data to file1 and file2, returns the errors. func ParallelWrite(data []byte) chan error { res := make(chan error, 2) f1, err := os.Create("file1") if err != nil { res <- err } else { 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. if err != nil { res <- err } else { go func() { _, err = f2.Write(data) res <- err f2.Close() }() } return res }
修正方法は、ゴルーチンに新しい変数を導入することです(:=
の使用に注意してください)。
... _, err := f1.Write(data) ... _, err := f2.Write(data) ...
保護されていないグローバル変数
次のコードが複数のゴルーチンから呼び出されると、service
マップでレースが発生します。同じマップの同時読み取りと書き込みは安全ではありません。
var service map[string]net.Addr func RegisterService(name string, addr net.Addr) { service[name] = addr } func LookupService(name string) net.Addr { return service[name] }
コードを安全にするには、mutexを使用してアクセスを保護します。
var ( service map[string]net.Addr serviceMu sync.Mutex ) func RegisterService(name string, addr net.Addr) { serviceMu.Lock() defer serviceMu.Unlock() service[name] = addr } func LookupService(name string) net.Addr { serviceMu.Lock() defer serviceMu.Unlock() return service[name] }
保護されていないプリミティブ変数
データレースは、この例のように、プリミティブ型の変数(bool
、int
、int64
など)でも発生する可能性があります。
type Watchdog struct{ last int64 } func (w *Watchdog) KeepAlive() { w.last = time.Now().UnixNano() // First conflicting access. } func (w *Watchdog) Start() { go func() { for { time.Sleep(time.Second) // Second conflicting access. if w.last < time.Now().Add(-10*time.Second).UnixNano() { fmt.Println("No keepalives for 10 seconds. Dying.") os.Exit(1) } } }() }
このような「無害な」データレースでさえ、メモリアクセスの非アトミック性、コンパイラ最適化への干渉、またはプロセッサメモリへのアクセスにおけるリオーダリングの問題によって、デバッグが難しい問題を引き起こす可能性があります。
このレースの典型的な修正方法は、チャネルまたはmutexを使用することです。ロックフリーの動作を維持するために、sync/atomic
パッケージを使用することもできます。
type Watchdog struct{ last int64 } func (w *Watchdog) KeepAlive() { atomic.StoreInt64(&w.last, time.Now().UnixNano()) } func (w *Watchdog) Start() { go func() { for { time.Sleep(time.Second) if atomic.LoadInt64(&w.last) < time.Now().Add(-10*time.Second).UnixNano() { fmt.Println("No keepalives for 10 seconds. Dying.") os.Exit(1) } } }() }
同期されていない送信およびクローズ操作
この例が示すように、同じチャネルでの同期されていない送信およびクローズ操作も、レース状態になる可能性があります。
c := make(chan struct{}) // or buffered channel // The race detector cannot derive the happens before relation // for the following send and close operations. These two operations // are unsynchronized and happen concurrently. go func() { c <- struct{}{} }() close(c)
Goメモリモデルによると、チャネルでの送信は、そのチャネルからの対応する受信が完了する前に発生します。送信およびクローズ操作を同期するには、送信がクローズ前に完了することを保証する受信操作を使用します。
c := make(chan struct{}) // or buffered channel go func() { c <- struct{}{} }() <-c close(c)
要件
レース検出器では、cgoを有効にする必要があり、非DarwinシステムではCコンパイラがインストールされている必要があります。レース検出器は、linux/amd64
、linux/ppc64le
、linux/arm64
、linux/s390x
、freebsd/amd64
、netbsd/amd64
、darwin/amd64
、darwin/arm64
、およびwindows/amd64
をサポートしています。
Windowsでは、レース検出器のランタイムはインストールされているCコンパイラのバージョンに依存します。Go 1.21の時点では、-race
でプログラムをビルドするには、mingw-w64
ランタイムライブラリのバージョン8以降を組み込んだCコンパイラが必要です。Cコンパイラを引数--print-file-name libsynchronization.a
を指定して呼び出すことで、Cコンパイラをテストできます。新しい準拠Cコンパイラは、このライブラリのフルパスを出力しますが、古いCコンパイラは単に引数をエコーするだけです。
ランタイムオーバーヘッド
レース検出のコストはプログラムによって異なりますが、一般的なプログラムでは、メモリ使用量が5〜10倍、実行時間が2〜20倍増加する可能性があります。
レース検出器は現在、defer
およびrecover
ステートメントごとに8バイト余分に割り当てます。これらの余分な割り当ては、ゴルーチンが終了するまで回復されません。つまり、defer
およびrecover
呼び出しを定期的に発行する長時間実行のゴルーチンがある場合、プログラムのメモリ使用量が際限なく増加する可能性があります。これらのメモリ割り当ては、runtime.ReadMemStats
またはruntime/pprof
の出力には表示されません。