[インデックス 19220] ファイルの概要
このコミットは、Goランタイムのデータ競合検出器(Race Detector)のテストスイートに、特定の既知の問題(Issue 7561)を再現するための新しいテストケースを追加します。このテストは、複数の戻り値を持つ関数の結果をマップの要素に代入する際に発生しうるデータ競合を検出することを目的としています。
コミット
commit 1332eb5b6210e16601ff8d049885e41a6e16908d
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Mon Apr 21 17:21:09 2014 +0200
runtime/race: add test for issue 7561.
LGTM=dvyukov
R=rsc, iant, khr, dvyukov
CC=golang-codereviews
https://golang.org/cl/76520045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1332eb5b6210e16601ff8d049885e41a6e16908d
元コミット内容
このコミットは、src/pkg/runtime/race/testdata/map_test.go
ファイルに18行のコードを追加します。追加されるのは TestRaceMapAssignMultipleReturn
という新しいテスト関数で、Issue 7561に関連するデータ競合をテストするためのものです。
変更の背景
このコミットは、Goのデータ競合検出器が特定のシナリオでデータ競合を正しく検出できない、または誤検出する可能性があったIssue 7561に対応するために、その問題のテストケースを追加するものです。
Issue 7561は、Goコンパイラ(特に src/cmd/gc/racewalk.c
)がコードを解析する際に生成する OBLOCK
ノードの扱いに関連するバグでした。OBLOCK
ノードは、複数のステートメントをグループ化するブロックを表し、レース検出器がこれらのブロック内のメモリ操作を正しく追跡することが重要です。
このコミットが提案された時点では、Issue 7561の根本的な問題は、他のコンパイラ変更によって偶発的に修正されていました。しかし、このテストケースを追加することで、将来的に同様の回帰が発生しないようにするためのガードとして機能します。つまり、問題が修正されたことを確認し、その修正が維持されることを保証するためのテストとして導入されました。
前提知識の解説
Goのデータ競合検出器 (Race Detector)
Goのデータ競合検出器は、並行処理を行うGoプログラムにおけるデータ競合(Data Race)を検出するための強力なツールです。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生します。
- 目的: データ競合はプログラムの予測不能な動作、クラッシュ、またはデータの破損を引き起こす可能性があるため、これらを早期に発見し修正することが目的です。
- 動作原理:
- Goプログラムを
-race
フラグ付きでビルドまたは実行(例:go run -race main.go
またはgo test -race
)すると、コンパイラはすべてのメモリアクセスを計測するコードを挿入します。 - ランタイムライブラリは、共有変数への非同期アクセスを監視します。
- 競合状態が検出されると、詳細な警告メッセージが出力され、競合が発生した場所と関連するゴルーチンのスタックトレースが示されます。
- Goプログラムを
- 特徴:
- 偽陽性なし: データ競合検出器は、偽陽性(実際には競合ではないが競合として報告されること)を生成しないように設計されています。報告された競合は、実際のバグを示します。
- ランタイム検出: 競合はプログラムの実行中にのみ検出されます。したがって、競合が発生する可能性のあるコードパスが実際に実行されるようなテストカバレッジやワークロードが重要です。
- パフォーマンスオーバーヘッド: 競合検出器を有効にすると、メモリ使用量(5〜10倍)と実行時間(2〜20倍)が大幅に増加する可能性があります。そのため、通常は開発およびテスト段階でのみ使用されます。
データ競合 (Data Race)
データ競合は、並行プログラミングにおける最も一般的なバグの一つです。以下の3つの条件がすべて満たされたときに発生します。
- 少なくとも2つのゴルーチンが同時に同じメモリ位置にアクセスする。
- 少なくとも1つのアクセスが書き込み操作である。
- それらのアクセスが、ミューテックス(
sync.Mutex
)やチャネルなどの同期メカニズムによって保護されていない。
データ競合が発生すると、プログラムの実行順序が非決定的になり、結果としてプログラムの動作が予測不能になります。これは、デバッグが非常に困難な「時々発生する」バグ(Heisenbug)につながることがよくあります。
Goにおける並行処理とマップ
Goはゴルーチンとチャネルによって強力な並行処理をサポートしていますが、共有メモリへのアクセスには注意が必要です。特にGoの組み込みマップは、複数のゴルーチンから同時に読み書きされると安全ではありません。このようなアクセスはデータ競合を引き起こし、パニックや不正なデータ状態につながる可能性があります。マップを並行して安全に使用するには、sync.Mutex
を使用してアクセスを保護するか、sync.Map
のような並行処理に対応したデータ構造を使用する必要があります。
OBLOCK
ノード (Compiler context)
OBLOCK
ノードは、Goコンパイラの内部表現における概念です。コンパイラがソースコードを抽象構文木(AST)に変換する際、複数のステートメントを論理的なブロックとしてグループ化するために使用されます。例えば、if
ステートメントの本体やループの本体などが OBLOCK
ノードとして表現されることがあります。
データ競合検出器は、コンパイラが生成したこのASTをウォーク(走査)し、メモリアクセスを特定して計測コードを挿入します。Issue 7561は、この OBLOCK
ノードの処理において、レース検出器が特定のメモリ操作を正しく追跡できない、または誤って解釈する問題があったことを示唆しています。
技術的詳細
このコミットは、Goのデータ競合検出器のテストスイートに TestRaceMapAssignMultipleReturn
という新しいテストケースを追加します。このテストは、Issue 7561で報告された、コンパイラが生成する OBLOCK
ノードに関連するデータ競合の誤検出または見逃しを防ぐために設計されました。
具体的には、このテストは以下のシナリオをシミュレートします。
connect
という関数が定義されており、これは複数の戻り値(int
とerror
)を返します。conns
というマップが作成され、その要素はスライス([]int
)です。- メインゴルーチンと別のゴルーチンが同時に
conns
マップの同じ要素(conns[1][0]
)にアクセスします。- 一方のゴルーチンは、
connect()
の複数の戻り値をconns[1][0]
とerr
に代入しようとします。 - もう一方のゴルーチン(メインゴルーチン)は、
conns[1][0]
の値を読み取ろうとします。
- 一方のゴルーチンは、
このような同時アクセスは、同期メカニズムなしに行われるため、データ競合を引き起こします。レース検出器は、このような複雑な代入操作(特に複数の戻り値とマップ要素へのアクセスが絡む場合)においても、共有メモリへのアクセスを正確に識別し、競合を報告できる必要があります。
Issue 7561は、コンパイラが OBLOCK
ノードを処理する際に、レース検出器がこれらのアクセスを正しく追跡できないケースがあったことを示しています。このテストは、その特定のケースを再現し、レース検出器が期待通りに動作することを確認するためのものです。
このテストが追加された時点では、根本的な問題は既に他のコンパイラ変更によって偶発的に修正されていました。しかし、このテストは、将来的にコンパイラの変更によって同様の問題が再発しないようにするための重要な回帰テストとして機能します。
コアとなるコードの変更箇所
変更は src/pkg/runtime/race/testdata/map_test.go
ファイルに集中しており、以下の新しいテスト関数が追加されています。
--- a/src/pkg/runtime/race/testdata/map_test.go
+++ b/src/pkg/runtime/race/testdata/map_test.go
@@ -198,6 +198,7 @@ func TestRaceMapDeletePartKey(t *testing.T) {
delete(m, *k)
<-ch
}
+
func TestRaceMapInsertPartKey(t *testing.T) {
k := &Big{}
m := make(map[Big]bool)
@@ -209,6 +210,7 @@ func TestRaceMapInsertPartKey(t *testing.T) {
m[*k] = true
<-ch
}
+
func TestRaceMapInsertPartVal(t *testing.T) {
v := &Big{}
m := make(map[int]Big)
@@ -220,3 +222,19 @@ func TestRaceMapInsertPartVal(t *testing.T) {
m[1] = *v
<-ch
}
+
+// Test for issue 7561.
+func TestRaceMapAssignMultipleReturn(t *testing.T) {
+ connect := func() (int, error) { return 42, nil }
+ conns := make(map[int][]int)
+ conns[1] = []int{0}
+ ch := make(chan bool, 1)
+ var err error
+ go func() {
+ conns[1][0], err = connect()
+ ch <- true
+ }()
+ x := conns[1][0]
+ _ = x
+ <-ch
+}
コアとなるコードの解説
追加された TestRaceMapAssignMultipleReturn
関数は、以下のように動作します。
-
connect := func() (int, error) { return 42, nil }
connect
という匿名関数を定義します。この関数はint
とerror
の2つの値を返します。ここでは常に42
とnil
を返します。
-
conns := make(map[int][]int)
- キーが
int
、値が[]int
(整数のスライス)であるマップconns
を作成します。
- キーが
-
conns[1] = []int{0}
conns
マップのキー1
に、要素が1つ(値は0
)のスライス[]int{0}
を代入します。これにより、conns[1][0]
という要素がアクセス可能になります。
-
ch := make(chan bool, 1)
- バッファサイズが1のチャネル
ch
を作成します。これはゴルーチン間の同期に使用されます。
- バッファサイズが1のチャネル
-
var err error
error
型の変数err
を宣言します。これはゴルーチン内でconnect()
の戻り値を受け取るために使用されます。
-
go func() { ... }()
- 新しいゴルーチンを起動します。このゴルーチン内でデータ競合を発生させます。
conns[1][0], err = connect()
: この行が競合の核心です。connect()
関数から返される複数の値が、conns[1][0]
(マップの要素)とローカル変数err
に同時に代入されます。ch <- true
: 代入が完了した後、チャネルに値を送信して、メインゴルーチンに処理が完了したことを通知します。
-
x := conns[1][0]
- メインゴルーチンで、ゴルーチンが
conns[1][0]
に書き込みを行っている最中に、同じconns[1][0]
の値を読み取ろうとします。これにより、書き込みと読み込みの間に同期がないため、データ競合が発生します。
- メインゴルーチンで、ゴルーチンが
-
_ = x
x
が未使用であることによるコンパイラエラーを避けるための慣用的な記述です。
-
<-ch
- メインゴルーチンは、ゴルーチンがチャネルに値を送信するまでブロックします。これにより、ゴルーチンが
conns[1][0]
への書き込みを完了するまで待機し、テストが確実に競合を検出できるようにします。
- メインゴルーチンは、ゴルーチンがチャネルに値を送信するまでブロックします。これにより、ゴルーチンが
このテストは、複数の戻り値の代入とマップ要素へのアクセスが絡む複雑なシナリオで、Goのデータ競合検出器が正しく機能することを確認するために設計されています。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/1332eb5b6210e16601ff8d049885e41a6e16908d
- Go Code Review (CL) 76520045: https://golang.org/cl/76520045
参考にした情報源リンク
- Go Code Review (CL) 76520045 の内容 (web_fetch ツールで取得)
- Goのデータ競合検出器に関する一般的な知識
- Goコンパイラの内部構造に関する一般的な知識