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

[インデックス 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.Fatalruntime.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.Logb.Error、そして削除された b.Fatal を呼び出した場合に、ベンチマーク全体がクラッシュしたりデッドロックしたりすることなく、適切に処理されることを検証しようとしていました。

しかし、コミットメッセージが示唆するように、Fatal must not be called from secondary goroutines. という原則があります。これは、b.Fatal が呼び出されたゴルーチンを即座に終了させる runtime.Goexit() を内部的に使用するため、RunParallel によって起動されたセカンダリゴルーチンから Fatal を呼び出すと、ベンチマークの制御フローが予期せぬ状態になり、テストフレームワークが適切にクリーンアップできない可能性があるためです。

このコミットは、このテストケースが「不正なベンチマーク」の例を含んでいたことを認識し、その不正な部分(セカンダリゴルーチンからの b.Fatal 呼び出し)を削除することで、テストの意図を修正しました。つまり、RunParallel のワーカーゴルーチンは b.Fatal を呼び出すべきではないという設計上の制約を明確にし、そのような誤用をテストケースから排除したものです。これにより、testing パッケージの堅牢性と、Fatal メソッドの適切な使用方法に関する理解が向上します。

関連リンク

参考にした情報源リンク

  • 上記のGitHubのコミットページと関連するGo issue。
  • Go言語の公式ドキュメント(testing パッケージ、runtime パッケージ)。
  • Go言語のテストとベンチマークに関する一般的な情報源。
  • Go言語の並行処理に関する情報源。
  • testing.B.RunParallel の動作に関する議論。
  • testing.B.Fatalruntime.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.