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

[インデックス 11185] ファイルの概要

このコミットは、Go言語の標準ライブラリsync/atomicパッケージのテストコードにおけるデータ競合を修正するものです。具体的には、テスト内で共有される変数への書き込みがアトミックに行われるよう、通常の代入操作をsync/atomicパッケージのStoreInt32およびStoreInt64関数に置き換えることで、テストの信頼性を向上させています。

コミット

sync/atomic: fix data race in tests
Fixes #2710.

R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/5541066

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/ba7dc5de064f7db4a41da2fd75757b46eca16ef5

元コミット内容

sync/atomic: fix data race in tests
Fixes #2710.

R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/5541066

変更の背景

このコミットは、Go言語のsync/atomicパッケージのテストコード、特にTestStoreLoadSeqCst32TestStoreLoadSeqCst64関数内で発生していたデータ競合(data race)を修正するために行われました。

これらのテストは、sync/atomicパッケージが提供するアトミックなStoreおよびLoad操作が「シーケンシャル一貫性(Sequential Consistency)」を持つことを検証することを目的としています。シーケンシャル一貫性とは、並行に実行される操作が、あたかも単一のプロセッサ上で逐次的に実行されたかのように見えるという強力なメモリモデルです。

しかし、テストコードの内部で、共有されるackという配列への書き込みが、通常のGoの代入操作(例: ack[me][(i-1)%3] = -1)で行われていました。複数のゴルーチンが同時にこのack配列にアクセスし、書き込みを行う場合、この通常の代入操作はアトミック性が保証されません。つまり、複数のゴルーチンによる書き込みが途中で割り込まれたり、部分的にしか完了していない状態で他のゴルーチンから読み取られたりする可能性があり、これがデータ競合を引き起こしていました。

データ競合が発生すると、テストが不安定になり、本来は問題ないはずのコードがテスト環境のタイミングによって失敗する「スパリアスな失敗(spurious failure)」を引き起こすことがあります。これは、テストの信頼性を著しく損なうため、修正が必要でした。この問題は、GoのIssueトラッカーで#2710として報告されていました。

前提知識の解説

データ競合 (Data Race)

データ競合とは、並行プログラミングにおいて発生する特定の競合状態(race condition)の一種です。以下の3つの条件がすべて満たされた場合に発生します。

  1. 複数のゴルーチン(またはスレッド)が同時に同じメモリ位置にアクセスしている。
  2. それらのアクセスのうち、少なくとも1つが書き込み操作である。
  3. それらのアクセスが、適切な同期メカニズム(ミューテックス、チャネル、アトミック操作など)によって同期されていない。

データ競合が発生すると、プログラムの動作が予測不能になり、未定義の動作(undefined behavior)を引き起こす可能性があります。例えば、変数の値が意図しないものになったり、プログラムがクラッシュしたり、デバッグが非常に困難なバグが発生したりします。

Goのsync/atomicパッケージ

Go言語の標準ライブラリsync/atomicパッケージは、低レベルなアトミック操作を提供します。アトミック操作とは、複数の操作が不可分(indivisible)であることを保証する操作です。つまり、その操作が完全に完了するか、全く実行されないかのどちらかであり、途中で他の操作に割り込まれることがありません。これにより、複数のゴルーチンが共有変数に同時にアクセスしても、データの一貫性が保たれ、データ競合を防ぐことができます。

sync/atomicパッケージは、主に以下のようなプリミティブなアトミック操作を提供します。

  • Load: 変数の値をアトミックに読み込む。
  • Store: 変数に値をアトミックに書き込む。
  • Add: 変数に値をアトミックに加算する。
  • Swap: 変数の値を新しい値とアトミックに交換し、元の値を返す。
  • CompareAndSwap (CAS): 変数の現在の値が期待する値と一致する場合にのみ、新しい値にアトミックに更新する。

これらの操作は、CPUのハードウェアレベルでのサポートを利用しており、ミューテックスなどの高レベルな同期メカニズムよりも高速に動作することが多いです。

StoreInt32 / StoreInt64

sync/atomicパッケージ内の具体的な関数で、それぞれ32ビット整数と64ビット整数をアトミックに書き込むために使用されます。

  • func StoreInt32(addr *int32, val int32): addrが指すint32型の変数にvalをアトミックに書き込みます。
  • func StoreInt64(addr *int64, val int64): addrが指すint64型の変数にvalをアトミックに書き込みます。

これらの関数は、引数としてポインタ(*int32*int64)を取るため、書き込み対象の変数のアドレスを渡す必要があります。

シーケンシャル一貫性 (Sequential Consistency)

シーケンシャル一貫性は、並行プログラムの実行モデル(メモリモデル)の一つで、最も直感的で強力な保証を提供します。シーケンシャル一貫性が保証されるシステムでは、すべてのプロセッサ(またはゴルーチン)からのすべての操作が、あたかも単一のプロセッサ上で逐次的に実行されたかのように見える、あるグローバルな順序が存在します。この順序は、各プロセッサが自身の操作をプログラム順に実行するという制約を満たします。

つまり、プログラムの実行結果は、すべての操作が何らかの単一の順序で実行され、かつ各ゴルーチン内の操作はそのゴルーチンが記述された順序で実行された場合と同じになります。これにより、並行プログラムの推論が容易になりますが、その分、実装には高いコストがかかる場合があります。sync/atomicパッケージの操作は、このシーケンシャル一貫性を保証するように設計されています。

技術的詳細

このコミットで修正されたTestStoreLoadSeqCst32およびTestStoreLoadSeqCst64テストは、sync/atomicパッケージのStoreおよびLoad操作がシーケンシャル一貫性を持つことを検証するために設計されています。これらのテストは、複数のゴルーチンを起動し、共有されるackという配列に対してアトミックな読み書き操作を繰り返し実行します。そして、最終的なack配列の状態を検証することで、操作の順序が期待通りであったかを確認します。

元のコードでは、ack配列の要素に値を書き込む際に、以下のような通常の代入操作が使われていました。

ack[me][(i-1)%3] = -1

Go言語において、int32int64のようなプリミティブ型の変数への通常の代入操作は、単一のCPU命令で実行されることが多いため、多くの場合アトミックであると誤解されがちです。しかし、これは保証されていません。特に、コンパイラの最適化や、複数のCPUコアがキャッシュを介してメモリにアクセスする際のメモリバリアの欠如などにより、データ競合が発生する可能性があります。

このテストは並行環境で実行されるため、複数のゴルーチンが同時にack配列の異なる(または同じ)要素に書き込もうとします。通常の代入では、この書き込み操作が不可分ではないため、以下のような問題が発生する可能性があります。

  1. 部分的な書き込み: あるゴルーチンが値を書き込んでいる最中に、別のゴルーチンがその値を読み取ろうとすると、部分的に更新された(不正な)値を読み取ってしまう可能性がある。
  2. 書き込みの順序の乱れ: 複数のゴルーチンによる書き込みが、期待される順序とは異なる順序でメモリに反映される可能性がある。

これらの問題は、テストが検証しようとしている「シーケンシャル一貫性」の前提をテスト自身の内部で破ってしまうことになり、テストが不安定になったり、誤った結果を報告したりする原因となります。

このコミットでは、この問題を解決するために、通常の代入操作をsync/atomicパッケージが提供するアトミックな書き込み関数に置き換えました。

// 変更前
ack[me][(i-1)%3] = -1

// 変更後
StoreInt32(&ack[me][(i-1)%3], -1) // TestStoreLoadSeqCst32の場合
StoreInt64(&ack[me][(i-1)%3], -1) // TestStoreLoadSeqCst64の場合

StoreInt32およびStoreInt64関数を使用することで、ack配列の要素への-1の書き込み操作が、CPUのハードウェアレベルで不可分に実行されることが保証されます。これにより、他のゴルーチンが同時にアクセスしても、書き込み操作が完全に完了してから次の操作が開始されるため、データ競合が完全に解消されます。

結果として、テストはより安定し、sync/atomicパッケージの実際のシーケンシャル一貫性の特性を正確に検証できるようになります。これは、Go言語の並行処理プリミティブの品質と信頼性を保証する上で非常に重要な修正です。

コアとなるコードの変更箇所

変更はsrc/pkg/sync/atomic/atomic_test.goファイル内で行われました。

TestStoreLoadSeqCst32 関数内 (約1037行目)

--- a/src/pkg/sync/atomic/atomic_test.go
+++ b/src/pkg/sync/atomic/atomic_test.go
@@ -1037,7 +1037,7 @@ func TestStoreLoadSeqCst32(t *testing.T) {
 			if my != i && his != i {
 				t.Fatalf("store/load are not sequentially consistent: %d/%d (%d)", my, his, i)
 			}
-			ack[me][(i-1)%3] = -1
+			StoreInt32(&ack[me][(i-1)%3], -1)
 		}
 		c <- true
 	}(p)

TestStoreLoadSeqCst64 関数内 (約1078行目)

--- a/src/pkg/sync/atomic/atomic_test.go
+++ b/src/pkg/sync/atomic/atomic_test.go
@@ -1078,7 +1078,7 @@ func TestStoreLoadSeqCst64(t *testing.T) {
 			if my != i && his != i {
 				t.Fatalf("store/load are not sequentially consistent: %d/%d (%d)", my, his, i)
 			}
-			ack[me][(i-1)%3] = -1
+			StoreInt64(&ack[me][(i-1)%3], -1)
 		}
 		c <- true
 	}(p)

コアとなるコードの解説

上記の変更は、ackという共有配列の要素に-1を書き込む操作を修正しています。

  • 変更前 (ack[me][(i-1)%3] = -1): これはGo言語における通常の変数への代入操作です。この操作は、単一のCPU命令で実行されることが多いですが、並行環境下ではそのアトミック性が保証されません。特に、コンパイラの最適化や、複数のCPUコアがキャッシュを介してメモリにアクセスする際のメモリバリアの欠如などにより、データ競合が発生する可能性があります。テストが複数のゴルーチンを起動して並行に実行されるため、この非アトミックな書き込みがテストの不安定性や誤った結果の原因となっていました。

  • 変更後 (StoreInt32(&ack[me][(i-1)%3], -1) および StoreInt64(&ack[me][(i-1)%3], -1)): sync/atomicパッケージのStoreInt32およびStoreInt64関数は、指定されたメモリアドレス(ここではack配列の要素のアドレス)に値をアトミックに書き込みます。アトミック操作は、その操作が完全に完了するまで他の操作に割り込まれないことを保証します。これにより、複数のゴルーチンが同時にack配列の同じ要素にアクセスしようとしても、書き込み操作が不可分に実行され、データ競合が確実に防止されます。

この修正により、テストコード自体がデータ競合から解放され、sync/atomicパッケージの本来の目的である「アトミック操作のシーケンシャル一貫性」を、より正確かつ安定して検証できるようになりました。これは、Go言語の並行処理プリミティブの堅牢性を保証する上で重要な改善です。

関連リンク

参考にした情報源リンク