Goのtesting/synctestパッケージについて
つぎのドキュメントを元に、testing/synctestについて確認したいと思います。
Package synctest provides support for testing concurrent code.
まずsynctestの用途ですが、このパッケージは並行処理のテストをサポートするパッケージであると説明されています。
それでは、synctestに用意されている関数を確認していきます。
synctest.Test
func TestTime(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
start := time.Now() // always midnight UTC 2000-01-01
go func() {
time.Sleep(1 * time.Second)
t.Log(time.Since(start)) // always logs "1s"
}()
time.Sleep(2 * time.Second) // the goroutine above will run before this Sleep returns
t.Log(time.Since(start)) // always logs "2s"
})
}
ドキュメントに記載されているサンプルでは、synctest.Testが使用されています。
そして、synctest.Testは、つぎのように記載されています。
The Test function runs a function in an isolated "bubble". Any goroutines started within the bubble are also part of the bubble.
ここでは、Test関数がバブルと呼ばれる独立した環境を実行する。また、バブル内で起動したすべてのgoroutineもまたバブルの一部であると説明されています。
上記のテストを実行すると、コメントにあるように、"1s"と"2s"がログに出力されます。
つぎに、上記のサンプルコードの一部をコメントアウトして実行すると、エラーになります。
func TestTime(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
start := time.Now() // always midnight UTC 2000-01-01
go func() {
time.Sleep(1 * time.Second)
t.Log(time.Since(start)) // always logs "1s"
}()
// time.Sleep(2 * time.Second) // the goroutine above will run before this Sleep returns
// t.Log(time.Since(start)) // always logs "2s"
})
}
その際、つぎのようなメッセージが表示されました。
panic: deadlock: main bubble goroutine has exited but blocked goroutines remain [recovered, repanicked]
メインのバブルgoroutineは終了しているが、ブロックされたgoroutineが残っているとなっています。
ドキュメントでは、バブル内のすべてのgoroutineがdurably blockedの場合にのみ、バブル内の時間は進むとされています。
Time in a bubble only advances when every goroutine in the bubble is durably blocked.
このエラーになった理由は、つぎのようになったためと解釈しました。
- メインのgoroutineが中断せずに終了
- 子のgoroutineはtime.Sleepでdurably blockedになる
- バブル内のすべてのgoroutineがdurably blockedになるという条件が満たされない
- バブル内の時間が進まない
=> ブロックされた子のgoroutineは残ってしまっている
durably blocked とは
duraby blockedはドキュメントでは、つぎのように説明されています。
A goroutine in a bubble is "durably blocked" when it is blocked and can only be unblocked by another goroutine in the same bubble. A goroutine which can be unblocked by an event from outside its bubble is not durably blocked.
goroutineがブロックされて、かつ同一バブル内の他のgoroutineによってのみブロックが解除される状態になっていることを"durably blocked"です。
バブルの外部からのイベントによってブロックが解除可能なgoroutineはdurably blockedにはなりません。
synctest.Wait
つぎは synctest.Wait について確認します。
func TestWait(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
done := false
go func() {
done = true
}()
// Wait will block until the goroutine above has finished.
synctest.Wait()
t.Log(done) // always logs "true"
})
}
The Wait function blocks until all other goroutines in the bubble are durably blocked.
Wait関数は、バブル内のすべての他のgoroutineがdurably blockedになるまでブロックします。
上記のTestWait関数の内部でdone = trueに設定しているgoroutine(以下、子goroutine)はdurably blockedにはなっていません。
このことについては、つぎのように理解しました。
- synctest.Waitが呼ばれる
- 子goroutineが起動する(実行中になる)
- synctest.Waitは、実行中の他のすべてのgoroutineがdurably blockedになるまで待機する
- 子goroutineが終了する(実行中ではなくなった)
- 他のgoroutineがなくなったので、synctest.Waitはreturnする
synctest.Waitを使ったときに、goroutineが残ったままになった場合、エラーになることを確認したいと思います。
サンプルコードを変更して、子goroutineの内部でtime.Sleepを呼ぶようにしました。
func TestWait(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
done := false
go func() {
time.Sleep(1 * time.Second)
done = true
}()
synctest.Wait()
t.Log(done) // false
})
}
この場合、t.Log(done)の結果はfalseとなり、テストはつぎのエラーで失敗します。
panic: deadlock: main bubble goroutine has exited but blocked goroutines remain [recovered, repanicked]
このエラーが発生した理由を、つぎのように解釈しました。
- doneをfalseで初期化
- 子goroutineを起動
- synctest.Waitは子goroutineがdurably blockedになるのを待つ
- 子goroutineはtime.Sleepでdurably blockedになる
- synctest.Waitは子goroutineがdurably blockedになったのでreturnする
- doneはまだtrueになっていない
- t.Logの実行時はdoneがまだfalseのまま
=> 子goroutineが残ったままとなる
Discussion