[インデックス 18624] ファイルの概要
このコミットは、Go言語の標準ライブラリ testing
パッケージにおけるベンチマークテストの誤った挙動を修正するものです。具体的には、testing.B.RunParallel
内で b.Fatal
が呼び出された際に発生する問題を解決します。
コミット
commit cd13a57b0ade69b8c4c2e4fc4f1952abfa885929
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Feb 24 21:12:44 2014 +0400
testing: fix bogus benchmark
Fatal must not be called from secondary goroutines.
Fixes #7401.
LGTM=bradfitz
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/67820047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cd13a57b0ade69b8c4c2e4fc4f1952abfa885929
元コミット内容
testing: fix bogus benchmark
Fatal must not be called from secondary goroutines.
Fixes #7401.
変更の背景
Go言語の testing
パッケージは、ユニットテスト、ベンチマークテスト、および例(Example)の記述をサポートします。ベンチマークテストは、testing.B
型の引数を受け取る関数として定義され、go test -bench .
コマンドで実行されます。
testing.B
には、テストの失敗を報告するためのメソッドがいくつかあります。
b.Log
: ログメッセージを出力します。b.Error
: エラーを報告しますが、テストの実行は継続します。b.Fatal
: エラーを報告し、現在のテスト(またはベンチマーク)の実行を即座に停止します。
このコミットが修正しようとしている問題は、testing.B.RunParallel
メソッドに関連しています。RunParallel
は、ベンチマーク関数を複数のゴルーチンで並行して実行するための機能です。これにより、マルチコア環境での性能特性を評価できます。
問題は、RunParallel
によって起動された「セカンダリゴルーチン」(メインのベンチマークゴルーチンではないもの)から b.Fatal
が呼び出された場合に発生しました。Fatal
は現在のテストの実行を停止する設計ですが、並行実行されている複数のゴルーチンがある場合、セカンダリゴルーチンからの Fatal
呼び出しが、ベンチマーク全体のクラッシュやデッドロックを引き起こす可能性がありました。これは、Fatal
が内部的に runtime.Goexit()
を呼び出してゴルーチンを終了させようとするため、他の並行ゴルーチンとの同期が取れなくなり、予期せぬ挙動につながるためです。
この問題は、Go issue #7401 として報告されていました。
前提知識の解説
Go言語の testing
パッケージ
Go言語の標準ライブラリの一部であり、テストコードを書くためのフレームワークを提供します。
testing.T
: ユニットテストで使用され、テストの成功/失敗の報告、ログ出力などを行います。testing.B
: ベンチマークテストで使用され、コードの性能測定を行います。N
回の操作を繰り返し実行し、その平均時間を測定します。testing.M
: テストのメイン関数を制御するために使用されます。
testing.B.RunParallel
ベンチマークテストを並行して実行するためのメソッドです。このメソッドは、ベンチマーク関数を複数のゴルーチンで実行し、各ゴルーチンが独立してベンチマーク対象のコードを実行します。これにより、並行処理のオーバーヘッドや競合状態の影響を考慮した性能測定が可能になります。
RunParallel
の内部では、b.SetParallelism(n)
で設定された並行度(デフォルトは GOMAXPROCS
)に基づいて、複数のワーカーゴルーチンが起動されます。これらのワーカーゴルーチンは、メインのベンチマークゴルーチンとは異なる「セカンダリゴルーチン」として扱われます。
testing.B.Fatal
と runtime.Goexit()
testing.B.Fatal
メソッドは、テストまたはベンチマークの実行中に致命的なエラーが発生した場合に呼び出されます。このメソッドが呼び出されると、エラーメッセージがログに出力され、現在のテストの実行が即座に停止されます。
Fatal
の内部実装では、最終的に runtime.Goexit()
が呼び出されます。runtime.Goexit()
は、現在のゴルーチンを終了させますが、プログラム全体は終了させません。これは、panic
とは異なり、defer
関数は実行されますが、呼び出し元のスタックをアンワインドすることなく、そのゴルーチンのみを終了させます。
並行処理の文脈では、runtime.Goexit()
は特定のゴルーチンを終了させるために使用されますが、他のゴルーチンは引き続き実行されます。このため、RunParallel
のセカンダリゴルーチンから Fatal
が呼び出されると、そのゴルーチンは終了しますが、他の並行ゴルーチンやメインのベンチマークゴルーチンは実行を継続しようとします。これにより、ベンチマークの制御フローが乱れ、デッドロックやクラッシュといった予期せぬ問題が発生する可能性がありました。
技術的詳細
このコミットの技術的詳細は、testing.B.RunParallel
の設計と testing.B.Fatal
の挙動の間の不整合にあります。
testing.B.RunParallel
は、ベンチマークの反復処理を複数のゴルーチンに分散させます。これらのゴルーチンは、b.RunParallel
に渡された匿名関数(ワーカー関数)を実行します。このワーカー関数内で b.Fatal
が呼び出されると、そのワーカーゴルーチンは runtime.Goexit()
によって終了します。
しかし、testing
パッケージの設計では、Fatal
はテスト(またはベンチマーク)全体を停止させることを意図しています。RunParallel
のコンテキストでは、これはすべての並行ワーカーゴルーチンとメインのベンチマークゴルーチンが適切に停止することを意味するべきです。セカンダリゴルーチンが単独で Goexit
してしまうと、メインのベンチマークロジックは、一部のワーカーが予期せず終了したことを適切に処理できず、結果としてベンチマークがハングアップしたり、不正確な結果を報告したり、最悪の場合、プログラム全体がクラッシュしたりする可能性がありました。
このコミットは、testing.B.RunParallel
のセカンダリゴルーチンから b.Fatal
を呼び出すこと自体が「不正なベンチマーク」であるという認識に基づいています。Fatal
は、テストのメインフローを制御する目的で使用されるべきであり、並行して実行されるワーカーゴルーチンから呼び出されるべきではありません。ワーカーゴルーチン内でエラーが発生した場合は、b.Error
を使用してエラーを報告し、ワーカーゴルーチンが正常に終了するようにするか、またはワーカーゴルーチンが致命的なエラーを検出した場合は、メインのベンチマークゴルーチンにその状態を適切に通知し、メインゴルーチンが b.Fatal
を呼び出すべきです。
この修正は、benchmark_test.go
内のテストコードから b.Fatal("fatal")
の行を削除することで、この「不正なベンチマーク」の例を取り除いています。これにより、RunParallel
のセカンダリゴルーチンから Fatal
を呼び出すという誤った使用パターンがテストされなくなり、その結果として発生する可能性のある問題が回避されます。これは、Fatal
のセマンティクスが並行ワーカーゴルーチンには適用されないという暗黙のルールを強化するものです。
コアとなるコードの変更箇所
src/pkg/testing/benchmark_test.go
ファイルの1行が削除されました。
--- a/src/pkg/testing/benchmark_test.go
+++ b/src/pkg/testing/benchmark_test.go
@@ -88,7 +88,6 @@ func TestRunParallelFail(t *testing.T) {
// w/o crashing/deadlocking the whole benchmark.
b.Log("log")
b.Error("error")
- b.Fatal("fatal")
})
})
具体的には、TestRunParallelFail
関数内の b.RunParallel
に渡される匿名関数(ワーカー関数)の中から b.Fatal("fatal")
の呼び出しが削除されました。
コアとなるコードの解説
削除された行は、testing
パッケージのベンチマークテストの内部テストケースである TestRunParallelFail
の一部でした。このテストは、b.RunParallel
内で b.Log
、b.Error
、そして削除された b.Fatal
を呼び出した場合に、ベンチマーク全体がクラッシュしたりデッドロックしたりすることなく、適切に処理されることを検証しようとしていました。
しかし、コミットメッセージが示唆するように、Fatal must not be called from secondary goroutines.
という原則があります。これは、b.Fatal
が呼び出されたゴルーチンを即座に終了させる runtime.Goexit()
を内部的に使用するため、RunParallel
によって起動されたセカンダリゴルーチンから Fatal
を呼び出すと、ベンチマークの制御フローが予期せぬ状態になり、テストフレームワークが適切にクリーンアップできない可能性があるためです。
このコミットは、このテストケースが「不正なベンチマーク」の例を含んでいたことを認識し、その不正な部分(セカンダリゴルーチンからの b.Fatal
呼び出し)を削除することで、テストの意図を修正しました。つまり、RunParallel
のワーカーゴルーチンは b.Fatal
を呼び出すべきではないという設計上の制約を明確にし、そのような誤用をテストケースから排除したものです。これにより、testing
パッケージの堅牢性と、Fatal
メソッドの適切な使用方法に関する理解が向上します。
関連リンク
- Go issue #7401: https://github.com/golang/go/issues/7401
- Go CL 67820047: https://golang.org/cl/67820047
testing
パッケージのドキュメント: https://pkg.go.dev/testingruntime.Goexit()
のドキュメント: https://pkg.go.dev/runtime#Goexit
参考にした情報源リンク
- 上記のGitHubのコミットページと関連するGo issue。
- Go言語の公式ドキュメント(
testing
パッケージ、runtime
パッケージ)。 - Go言語のテストとベンチマークに関する一般的な情報源。
- Go言語の並行処理に関する情報源。
testing.B.RunParallel
の動作に関する議論。testing.B.Fatal
とruntime.Goexit()
の関係に関する議論。 I have generated the detailed technical explanation in Markdown format, following all the specified instructions and chapter structure. The output is ready to be displayed.