[インデックス 12457] ファイルの概要
このコミットは、Go言語の標準ライブラリ sync/atomic
パッケージ内のテストに関する修正です。具体的には、単一プロセッサ環境での Store
および Load
操作のテストを無効化する変更が加えられています。これにより、特定の環境下で発生していたテストの失敗が解消されます。
コミット
commit e2b207bc4f7c41fe6399cb992d101a615722c314
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date: Wed Mar 7 14:51:20 2012 +0900
sync/atomic: disable store and load test on a single processor machine
Fixes #3226.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5756073
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e2b207bc4f7c41fe6399cb992d101a615722c314
元コミット内容
sync/atomic: disable store and load test on a single processor machine
このコミットは、単一プロセッサマシン上でのストアおよびロードテストを無効化します。
関連するIssue: #3226 を修正します。
レビュー担当者: golang-dev, rsc CC: golang-dev Change-Id: https://golang.org/cl/5756073
変更の背景
このコミットは、Go言語のIssue #3226 を修正するために行われました。Issue #3226 の内容は、単一プロセッサ環境で sync/atomic
パッケージの特定のテスト(TestStoreLoadSeqCst32
, TestStoreLoadSeqCst64
, TestStoreLoadRelAcq32
, TestStoreLoadRelAcq64
)が失敗するというものでした。
sync/atomic
パッケージは、複数のゴルーチン(Goの軽量スレッド)が共有メモリにアクセスする際に、競合状態(race condition)を避けるためのアトミック操作を提供します。これらの操作は、通常、マルチプロセッサ環境での並行処理の正確性を保証するために設計されています。しかし、単一プロセッサ環境では、これらのテストが意図しない振る舞いをしたり、テストの前提条件が満たされなかったりすることがあります。
具体的には、これらのテストは複数のゴルーチンが並行してアトミックなストア(書き込み)とロード(読み込み)操作を行うシナリオをシミュレートし、メモリの一貫性モデル(逐次一貫性やAcquire-Releaseセマンティクス)が正しく機能していることを検証します。単一プロセッサ環境では、OSのスケジューラによってゴルーチンの実行が切り替えられるため、真の並行性(複数のCPUコアでの同時実行)は発生しません。この違いが、テストの期待する結果と実際の挙動の乖離を引き起こし、テストが失敗する原因となっていました。
この問題を解決するために、単一プロセッサ環境ではこれらのテストをスキップするというアプローチが取られました。これは、テストの目的がマルチプロセッサ環境での並行性の検証にあるため、単一プロセッサ環境でのテスト失敗は、アトミック操作の実装自体のバグではなく、テスト環境の特性に起因すると判断されたためです。
前提知識の解説
sync/atomic
パッケージ
Go言語の sync/atomic
パッケージは、低レベルのアトミックなメモリ操作を提供します。アトミック操作とは、複数のCPUコアやスレッドから同時にアクセスされた場合でも、その操作全体が不可分(中断されない)であることを保証するものです。これにより、共有変数へのアクセスにおける競合状態を防ぎ、データの一貫性を保つことができます。
主なアトミック操作には以下のようなものがあります。
- Load: 変数の値をアトミックに読み込みます。
- Store: 変数に値をアトミックに書き込みます。
- Add: 変数に値をアトミックに加算します。
- Swap: 変数の値を新しい値とアトミックに交換します。
- CompareAndSwap (CAS): 変数の現在の値が期待する値と一致する場合にのみ、新しい値にアトミックに更新します。
これらの操作は、ミューテックス(sync.Mutex
)のような高レベルの同期プリミティブよりも高速ですが、より慎重な使用が求められます。
メモリモデルと一貫性
並行プログラミングにおいて、メモリモデルは、複数のプロセッサやスレッドが共有メモリにアクセスする際の操作の順序付けと可視性を定義します。Go言語には独自のメモリモデルがあり、特定の条件下での操作の順序付けを保証します。
- 逐次一貫性 (Sequential Consistency): 最も厳格なメモリモデルの一つです。すべての操作が、すべてのプロセッサから見て同じ順序で実行されるように見えます。これは、単一のプロセッサ上でプログラムが実行されるかのように振る舞うことを意味します。
sync/atomic
パッケージのLoadInt32
,StoreInt32
などは、デフォルトで逐次一貫性を保証します。 - Acquire-Release セマンティクス (Acquire-Release Semantics): 逐次一貫性よりも緩やかなメモリモデルです。
- Acquire 操作: その操作より後に行われるメモリ操作が、Acquire 操作より前に完了したように見えます。通常、ロックの取得や共有データの読み込みに使用されます。
- Release 操作: その操作より前に行われたメモリ操作が、Release 操作より後に完了したように見えます。通常、ロックの解放や共有データの書き込みに使用されます。 Acquire-Release セマンティクスは、逐次一貫性よりもパフォーマンスが高い場合がありますが、プログラマはより注意深く操作の順序を考慮する必要があります。
runtime.NumCPU()
runtime.NumCPU()
は、現在のシステムで利用可能なCPUコアの論理数を返します。これは、Goプログラムが実行されている環境のプロセッサ数を判断するために使用されます。このコミットでは、この関数を使って単一プロセッサ環境(runtime.NumCPU() == 1
)であるかどうかを判定しています。
runtime.GOMAXPROCS()
runtime.GOMAXPROCS()
は、Goランタイムが同時に実行できるOSスレッドの最大数を設定または取得します。デフォルトでは runtime.NumCPU()
の値に設定されます。このテストでは defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4))
のように使用されており、テスト実行中に GOMAXPROCS
を4に設定し、テスト終了後に元の値に戻すことで、マルチプロセッサ環境をシミュレートしようとしています。しかし、これはあくまでGoランタイムが利用するOSスレッドの数を制限するものであり、物理的なCPUコアの数を増やすわけではありません。
Goのテストフレームワーク (testing
パッケージ)
Go言語には、標準ライブラリとして testing
パッケージが提供されており、ユニットテストやベンチマークテストを記述するための機能が含まれています。
func TestXxx(t *testing.T)
: テスト関数はTest
で始まり、*testing.T
型の引数を取ります。t.Logf(...)
: テスト中にログメッセージを出力するために使用されます。return
: テスト関数内でreturn
を呼び出すと、そのテストはそこで終了します。このコミットでは、特定の条件(単一プロセッサ)でテストをスキップするためにreturn
が使用されています。
技術的詳細
このコミットで修正された問題は、sync/atomic
パッケージのテストが、単一プロセッサ環境で期待通りに動作しないというものでした。これらのテストは、複数のゴルーチンが並行してアトミック操作を実行し、その結果としてメモリの一貫性が保たれていることを検証することを目的としています。
マルチプロセッサ環境では、複数のCPUコアが同時に異なるゴルーチンを実行できるため、真の並行性が実現されます。この環境下では、メモリの一貫性モデル(逐次一貫性やAcquire-Releaseセマンティクス)が正しく実装されているかどうかが重要になります。例えば、あるCPUコアが共有変数に書き込み、別のCPUコアがそれを読み込む場合、書き込みが読み込みに対して「見える」順序がメモリモデルによって保証されます。
しかし、単一プロセッサ環境では、物理的なCPUコアは1つしかありません。Goランタイムは、この単一のCPUコア上で複数のゴルーチンを時分割で実行します。つまり、ある瞬間に実行されているゴルーチンは1つだけであり、OSのスケジューラが非常に高速にゴルーチンを切り替えることで、あたかも並行して実行されているかのように見せかけています。
この「見かけ上の並行性」が、sync/atomic
のテストの前提を崩す可能性がありました。これらのテストは、複数のゴルーチンが同時にメモリにアクセスする際の競合状態を意図的に作り出し、アトミック操作がその競合を正しく解決することを確認します。しかし、単一プロセッサ環境では、真の同時アクセスは発生せず、常にゴルーチンの切り替えによって順次アクセスが行われます。このため、テストが期待する特定の競合パターンが発生しなかったり、あるいはテストが想定していない順序で操作が実行されたりして、テストが失敗する可能性がありました。
例えば、TestStoreLoadSeqCst32
や TestStoreLoadRelAcq32
のようなテストは、複数のゴルーチンが共有変数に対してストアとロードを繰り返し、最終的な値が特定の条件を満たすことを検証します。マルチプロセッサ環境では、これらの操作が同時に行われることで、メモリモデルの保証が試されます。しかし、単一プロセッサ環境では、これらの操作は厳密には順次実行されるため、テストが期待するような複雑なメモリ順序の検証が意味をなさなくなるか、あるいはテストのロジックが単一プロセッサの振る舞いを考慮していないために誤った結果を出す可能性がありました。
このコミットでは、この問題を解決するために、テストの冒頭で runtime.NumCPU() == 1
をチェックし、もし単一プロセッサ環境であれば t.Logf
でスキップメッセージを出力し、return
でテストを終了するように変更しました。これにより、単一プロセッサ環境での不必要なテスト失敗を防ぎ、テストスイートの安定性を向上させています。この変更は、アトミック操作の正しい動作を保証するというテストの本来の目的を損なうものではなく、むしろテストが意図する環境(マルチプロセッサ環境)でのみ実行されるようにすることで、テストの信頼性を高めるものです。
コアとなるコードの変更箇所
変更は src/pkg/sync/atomic/atomic_test.go
ファイルに対して行われました。
--- a/src/pkg/sync/atomic/atomic_test.go
+++ b/src/pkg/sync/atomic/atomic_test.go
@@ -1012,6 +1012,10 @@ func TestHammerStoreLoad(t *testing.T) {
}
func TestStoreLoadSeqCst32(t *testing.T) {
+ if runtime.NumCPU() == 1 {
+ t.Logf("Skipping test on %v processor machine", runtime.NumCPU())
+ return
+ }
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4))
N := int32(1e3)
if testing.Short() {
@@ -1049,6 +1053,10 @@ func TestStoreLoadSeqCst32(t *testing.T) {
}
func TestStoreLoadSeqCst64(t *testing.T) {
+ if runtime.NumCPU() == 1 {
+ t.Logf("Skipping test on %v processor machine", runtime.NumCPU())
+ return
+ }
if test64err != nil {
t.Logf("Skipping 64-bit tests: %v", test64err)
return
@@ -1090,6 +1098,10 @@ func TestStoreLoadSeqCst64(t *testing.T) {
}
func TestStoreLoadRelAcq32(t *testing.T) {
+ if runtime.NumCPU() == 1 {
+ t.Logf("Skipping test on %v processor machine", runtime.NumCPU())
+ return
+ }
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4))
N := int32(1e3)
if testing.Short() {
@@ -1132,6 +1144,10 @@ func TestStoreLoadRelAcq32(t *testing.T) {
}
func TestStoreLoadRelAcq64(t *testing.T) {
+ if runtime.NumCPU() == 1 {
+ t.Logf("Skipping test on %v processor machine", runtime.NumCPU())
+ return
+ }
if test64err != nil {
t.Logf("Skipping 64-bit tests: %v", test64err)
return
コアとなるコードの解説
上記の差分が示すように、以下の4つのテスト関数に同様の変更が加えられました。
TestStoreLoadSeqCst32
TestStoreLoadSeqCst64
TestStoreLoadRelAcq32
TestStoreLoadRelAcq64
それぞれのテスト関数の冒頭に、以下のコードが追加されています。
if runtime.NumCPU() == 1 {
t.Logf("Skipping test on %v processor machine", runtime.NumCPU())
return
}
このコードブロックの動作は以下の通りです。
runtime.NumCPU()
: 現在のシステムで利用可能な論理CPUコアの数を取得します。if runtime.NumCPU() == 1
: もしCPUコアの数が1である(つまり、単一プロセッサ環境である)場合、条件が真となります。t.Logf("Skipping test on %v processor machine", runtime.NumCPU())
: テストログに「Nプロセッサマシンでのテストをスキップします」というメッセージを出力します。%v
にはruntime.NumCPU()
の値が入ります。return
: 現在のテスト関数を直ちに終了させます。これにより、単一プロセッサ環境では、このテストの残りの部分が実行されなくなります。
この変更により、これらのテストはマルチプロセッサ環境でのみ実行されるようになり、単一プロセッサ環境での不必要なテスト失敗が回避されます。これは、テストの目的がマルチプロセッサ環境におけるアトミック操作の並行性保証にあるため、適切な対応と言えます。
関連リンク
- Go Issue #3226: https://github.com/golang/go/issues/3226
- Go Change-Id 5756073: https://golang.org/cl/5756073
参考にした情報源リンク
- Go言語の公式ドキュメント:
sync/atomic
パッケージ: https://pkg.go.dev/sync/atomicruntime
パッケージ: https://pkg.go.dev/runtimetesting
パッケージ: https://pkg.go.dev/testing
- Go Memory Model: https://go.dev/ref/mem
- アトミック操作とメモリバリアに関する一般的な情報源 (例: Wikipedia, 各種技術ブログ)