Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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)すると、コンパイラはすべてのメモリアクセスを計測するコードを挿入します。
    • ランタイムライブラリは、共有変数への非同期アクセスを監視します。
    • 競合状態が検出されると、詳細な警告メッセージが出力され、競合が発生した場所と関連するゴルーチンのスタックトレースが示されます。
  • 特徴:
    • 偽陽性なし: データ競合検出器は、偽陽性(実際には競合ではないが競合として報告されること)を生成しないように設計されています。報告された競合は、実際のバグを示します。
    • ランタイム検出: 競合はプログラムの実行中にのみ検出されます。したがって、競合が発生する可能性のあるコードパスが実際に実行されるようなテストカバレッジやワークロードが重要です。
    • パフォーマンスオーバーヘッド: 競合検出器を有効にすると、メモリ使用量(5〜10倍)と実行時間(2〜20倍)が大幅に増加する可能性があります。そのため、通常は開発およびテスト段階でのみ使用されます。

データ競合 (Data Race)

データ競合は、並行プログラミングにおける最も一般的なバグの一つです。以下の3つの条件がすべて満たされたときに発生します。

  1. 少なくとも2つのゴルーチンが同時に同じメモリ位置にアクセスする。
  2. 少なくとも1つのアクセスが書き込み操作である。
  3. それらのアクセスが、ミューテックス(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 ノードに関連するデータ競合の誤検出または見逃しを防ぐために設計されました。

具体的には、このテストは以下のシナリオをシミュレートします。

  1. connect という関数が定義されており、これは複数の戻り値(interror)を返します。
  2. conns というマップが作成され、その要素はスライス([]int)です。
  3. メインゴルーチンと別のゴルーチンが同時に 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 関数は、以下のように動作します。

  1. connect := func() (int, error) { return 42, nil }

    • connect という匿名関数を定義します。この関数は interror の2つの値を返します。ここでは常に 42nil を返します。
  2. conns := make(map[int][]int)

    • キーが int、値が []int(整数のスライス)であるマップ conns を作成します。
  3. conns[1] = []int{0}

    • conns マップのキー 1 に、要素が1つ(値は 0)のスライス []int{0} を代入します。これにより、conns[1][0] という要素がアクセス可能になります。
  4. ch := make(chan bool, 1)

    • バッファサイズが1のチャネル ch を作成します。これはゴルーチン間の同期に使用されます。
  5. var err error

    • error 型の変数 err を宣言します。これはゴルーチン内で connect() の戻り値を受け取るために使用されます。
  6. go func() { ... }()

    • 新しいゴルーチンを起動します。このゴルーチン内でデータ競合を発生させます。
    • conns[1][0], err = connect(): この行が競合の核心です。connect() 関数から返される複数の値が、conns[1][0](マップの要素)とローカル変数 err に同時に代入されます。
    • ch <- true: 代入が完了した後、チャネルに値を送信して、メインゴルーチンに処理が完了したことを通知します。
  7. x := conns[1][0]

    • メインゴルーチンで、ゴルーチンが conns[1][0] に書き込みを行っている最中に、同じ conns[1][0] の値を読み取ろうとします。これにより、書き込みと読み込みの間に同期がないため、データ競合が発生します。
  8. _ = x

    • x が未使用であることによるコンパイラエラーを避けるための慣用的な記述です。
  9. <-ch

    • メインゴルーチンは、ゴルーチンがチャネルに値を送信するまでブロックします。これにより、ゴルーチンが conns[1][0] への書き込みを完了するまで待機し、テストが確実に競合を検出できるようにします。

このテストは、複数の戻り値の代入とマップ要素へのアクセスが絡む複雑なシナリオで、Goのデータ競合検出器が正しく機能することを確認するために設計されています。

関連リンク

参考にした情報源リンク

  • Go Code Review (CL) 76520045 の内容 (web_fetch ツールで取得)
  • Goのデータ競合検出器に関する一般的な知識
  • Goコンパイラの内部構造に関する一般的な知識