[インデックス 10973] ファイルの概要
このコミットは、Go言語の標準ライブラリであるtesting
パッケージにおける並列テストのシグナリングメカニズムを改善し、より安全にするための変更です。具体的には、各並列テストが独自のシグナルチャネルを持つようにすることで、テスト間の競合状態を防ぎ、並列テストが正しく実行されないバグを修正しています。
コミット
commit 66155134a7daa2a28bf0ecd55bcf36be3b21e473
Author: Rob Pike <r@golang.org>
Date: Thu Dec 22 10:43:54 2011 -0800
testing: make signalling safer for parallel tests
Each test gets a private signal channel.
Also fix a bug that prevented parallel tests from running.
R=r, r
CC=golang-dev
https://golang.org/cl/5505061
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/66155134a7daa2a28bf0ecd55bcf36be3b21e473
元コミット内容
testing: make signalling safer for parallel tests
Each test gets a private signal channel.
Also fix a bug that prevented parallel tests from running.
変更の背景
Goのtesting
パッケージでは、t.Parallel()
を呼び出すことでテストを並列実行させることができます。しかし、初期の実装では、並列テストの完了を通知するためのシグナルチャネルがテストランナー全体で共有されていました。この共有チャネルの設計には、以下の2つの主要な問題がありました。
- シグナリングの安全性と競合状態: 複数の並列テストが同じチャネルに完了シグナルを送るため、どのシグナルがどのテストからのものかを正確に識別することが困難でした。これにより、テストの完了カウントが誤ったり、意図しないテストが完了したと見なされたりする競合状態が発生する可能性がありました。特に、あるテストが失敗してすぐに終了した場合でも、その完了シグナルが他のテストの完了と混同され、テストランナーのロジックを狂わせる可能性がありました。コミットメッセージにある「If all tests pump to the same channel, a bug can occur where a goroutine kicks off a test, fails, and still delivers a completion signal, which skews the counting.」という記述がこの問題を示唆しています。
- 並列テストの実行阻害バグ: 共有チャネルのロジックに起因するバグにより、一部の並列テストが正しく開始または完了シグナルを送信できず、結果として並列テストが全く実行されない、あるいは途中で停止してしまう問題が発生していました。
これらの問題を解決し、testing
パッケージの並列テスト機能をより堅牢で信頼性の高いものにするために、このコミットが導入されました。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の概念とtesting
パッケージの基本的な動作を把握しておく必要があります。
- Goの
testing
パッケージ: Go言語に組み込まれているテストフレームワークです。go test
コマンドで実行され、TestXxx
という形式の関数をテストとして認識します。 *testing.T
: 各テスト関数に渡される構造体で、テストの実行状態(成功/失敗)、ログ出力、サブテストの実行、並列テストの制御など、テストに関する様々な機能を提供します。t.Parallel()
:*testing.T
のメソッドで、このメソッドが呼び出されたテストは、他の並列テストと共に並行して実行されることをテストランナーに通知します。これにより、テストの実行時間を短縮できます。- Goroutine: Go言語の軽量な並行処理単位です。数千、数万のgoroutineを同時に実行してもオーバーヘッドが少ないのが特徴です。
go
キーワードを使って関数をgoroutineとして起動します。 - チャネル (Channel): Goroutine間でデータを安全に送受信するためのGo言語のプリミティブです。チャネルは、並行処理における同期と通信の主要なメカニズムとして機能します。データはチャネルに送信され(
ch <- data
)、別のgoroutineがチャネルからデータを受信します(data := <-ch
)。チャネルはデフォルトでブロックするため、送信側は受信側が準備できるまで、受信側は送信側が準備できるまで待機します。これはgoroutine間の同期に利用されます。 runtime.GOMAXPROCS
: Goプログラムが同時に実行できるOSスレッドの最大数を設定する関数です。この値は、GoランタイムがgoroutineをOSスレッドにどのようにマッピングするかに影響します。testing
パッケージでは、異なるGOMAXPROCS
設定でテストを実行し、並行処理の挙動を確認することがあります。
技術的詳細
このコミットの核心は、並列テストの完了シグナルを処理する方法の根本的な変更にあります。
-
各テストへのプライベートシグナルチャネルの割り当て: 以前は、すべての並列テストが単一の共有
signal
チャネルを使用していました。この変更により、*testing.T
構造体の各インスタンス(つまり、各テスト)が独自のsignal
チャネルを持つようになりました。これにより、特定のテストからの完了シグナルが他のテストのシグナルと混同されることがなくなり、シグナリングの安全性が大幅に向上します。 -
collector
チャネルの導入: 各テストがプライベートなシグナルチャネルを持つようになったため、テストランナーはこれらの個別のチャネルからシグナルを収集し、全体としての並列テストの完了を監視する必要があります。このために、RunTests
関数内に新しいcollector
チャネルが導入されました。各並列テストが完了シグナルを自身のプライベートチャネルに送信すると、そのシグナルは新しいgoroutineによってcollector
チャネルに転送されます。テストランナーは、このcollector
チャネルからシグナルを受信することで、すべての並列テストの完了を効率的かつ正確に追跡できるようになります。これにより、共有チャネルの競合問題を解決しつつ、複数の並列テストの完了を単一の場所で集約できるようになります。 -
t.Parallel()
内のシグナル送信の変更:t.Parallel()
メソッドは、テストが並列実行されることをテストランナーに通知し、メインのテストループを解放するためにシグナルを送信します。以前はt.signal <- nil
としていましたが、これはinterface{}
型のチャネルにnil
を送信していました。変更後はt.signal <- (*T)(nil)
となり、*testing.T
型のnil
ポインタを送信するようになりました。これは、チャネルの型がinterface{}
であっても、送信する値の具体的な型を明示することで、より意図が明確になり、将来的な型安全性の向上に寄与します。このシグナルは、テストが並列実行モードに入ったことをテストランナーに伝えるためのものです。 -
並列テスト実行バグの修正: 共有チャネルの設計では、テストの開始と完了のシグナルが混在し、テストランナーが並列テストの数を正確にカウントできない、あるいは完了シグナルを誤って処理してしまうことがありました。各テストにプライベートチャネルを割り当て、
collector
チャネルを通じて集約する新しいメカニズムにより、シグナルが明確に分離され、テストランナーが並列テストのライフサイクルを正確に管理できるようになりました。これにより、以前は並列テストの実行を妨げていた根本的なバグが解消されました。
これらの変更により、Goのtesting
パッケージは、より大規模で複雑なテストスイートにおいても、並列テストを安全かつ信頼性高く実行できるようになりました。
コアとなるコードの変更箇所
変更はsrc/pkg/testing/testing.go
ファイルに集中しています。
--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -182,8 +182,8 @@ func (c *common) Fatalf(format string, args ...interface{}) {
// Parallel signals that this test is to be run in parallel with (and only with)
// other parallel tests in this CPU group.
func (t *T) Parallel() {
- t.signal <- nil // Release main testing loop
- <-t.startParallel // Wait for serial tests to finish
+ t.signal <- (*T)(nil) // Release main testing loop
+ <-t.startParallel // Wait for serial tests to finish
}
// An internal type but exported because it is cross-package; part of the implementation
@@ -236,11 +236,14 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
fmt.Fprintln(os.Stderr, "testing: warning: no tests to run")
return
}
- // TODO: each test should have its own channel, although that means
- // keeping track of the channels when we're running parallel tests.
- signal := make(chan interface{})
for _, procs := range cpuList {
runtime.GOMAXPROCS(procs)
+ // We build a new channel tree for each run of the loop.
+ // collector merges in one channel all the upstream signals from parallel tests.
+ // If all tests pump to the same channel, a bug can occur where a goroutine
+ // kicks off a test, fails, and still delivers a completion signal, which skews the
+ // counting.
+ var collector = make(chan interface{})
numParallel := 0
startParallel := make(chan bool)
@@ -260,7 +263,7 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
}
t := &T{
common: common{
- signal: signal,
+ signal: make(chan interface{}),
},
name: testName,
startParallel: startParallel,
@@ -272,6 +275,9 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
go tRunner(t, &tests[i])
out := (<-t.signal).(*T)
if out == nil { // Parallel run.
+ go func() {
+ collector <- <-t.signal
+ }()
numParallel++
continue
}
@@ -287,7 +293,7 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
numParallel--
continue
}
- t := (<-signal).(*T)
+ t := (<-collector).(*T)
t.report()
ok = ok && !t.failed
running--
コアとなるコードの解説
-
func (t *T) Parallel()
の変更:- 変更前:
t.signal <- nil
- 変更後:
t.signal <- (*T)(nil)
t.Parallel()
が呼び出された際、テストが並列実行モードに入ったことをテストランナーに通知するために、t.signal
チャネルにシグナルを送信します。以前はnil
を直接送信していましたが、これはinterface{}
型のチャネルにnil
を送信する際に、値がnil
であることと型がnil
であることの区別が曖昧になる可能性がありました。変更後は、明示的に*testing.T
型のnil
ポインタを送信することで、より型安全で意図が明確なシグナリングを実現しています。このシグナルは、テストランナーがテストを並列キューに入れるためのトリガーとなります。
- 変更前:
-
func RunTests(...)
内の変更:-
共有
signal
チャネルの削除:- 変更前:
signal := make(chan interface{})
- 変更後: この行が削除されました。
- 以前は、すべてのテストが完了シグナルを送るための単一の
signal
チャネルがRunTests
関数内で作成されていました。この共有チャネルが競合状態の原因となっていたため、削除されました。
- 変更前:
-
collector
チャネルの導入:- 変更後:
var collector = make(chan interface{})
RunTests
関数のfor _, procs := range cpuList
ループ内で、各GOMAXPROCS
設定の実行ごとに新しいcollector
チャネルが作成されるようになりました。このcollector
チャネルは、各並列テストが自身のプライベートチャネルに送信した完了シグナルを一つに集約するためのものです。これにより、テストランナーは単一のチャネルからすべての並列テストの完了を監視できるようになります。
- 変更後:
-
*testing.T
インスタンスのsignal
チャネルの初期化:- 変更前:
signal: signal,
- 変更後:
signal: make(chan interface{}),
- 各
*testing.T
インスタンスが作成される際に、以前は共有のsignal
チャネルを割り当てていましたが、変更後はmake(chan interface{})
によって各テストに専用のプライベートなシグナルチャネルが割り当てられるようになりました。これが「Each test gets a private signal channel.」というコミットメッセージの核心部分です。
- 変更前:
-
並列テストの完了シグナル転送ロジックの追加:
- 変更後:
if out == nil { // Parallel run. go func() { collector <- <-t.signal }() numParallel++ continue }
- テストが
t.Parallel()
を呼び出して並列実行モードに入った場合(out == nil
の場合)、新しいgoroutineが起動されます。このgoroutineは、そのテストのプライベートなt.signal
チャネルから完了シグナルを受信し、それをcollector
チャネルに転送します。これにより、テストランナーはcollector
チャネルを通じて、すべての並列テストの完了を非同期に、かつ安全に監視できるようになります。
- 変更後:
-
collector
チャネルからのシグナル受信:- 変更前:
t := (<-signal).(*T)
- 変更後:
t := (<-collector).(*T)
- テストランナーが並列テストの完了を待つ際、以前は共有の
signal
チャネルからシグナルを受信していましたが、変更後は新しく導入されたcollector
チャネルからシグナルを受信するようになりました。これにより、テストランナーは各テストのプライベートチャネルから集約された完了シグナルを処理し、テストのレポートや状態更新を正確に行うことができます。
- 変更前:
-
これらの変更により、Goのtesting
パッケージは、並列テストのシグナリングメカニズムを大幅に改善し、より堅牢で信頼性の高い並列テスト実行環境を提供できるようになりました。
関連リンク
- Go CL 5505061: https://golang.org/cl/5505061
参考にした情報源リンク
- コミットメッセージと差分 (
git diff
) - Go言語の公式ドキュメント (
testing
パッケージ、goroutine、チャネルに関する記述) - Go言語のソースコード (
src/pkg/testing/testing.go
)