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

[インデックス 16172] ファイルの概要

このコミットは、Go言語のテストスイート内にある test/goprint.go ファイルに対する変更です。具体的には、テストの安定性を向上させるために、ゴルーチンが実行されるのを待つ時間を延長しています。

コミット

このコミットは、test/goprint.go というテストファイルにおいて、main 関数内で起動されるゴルーチンがその処理を完了するのに十分な時間を与えるため、time.Sleep の待機時間を 1e6 (1ミリ秒) から 100*time.Millisecond (100ミリ秒) へと延長するものです。これにより、テストが不安定になる可能性のある競合状態を解消し、より信頼性の高いテスト実行を保証します。

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/4925f8aa79db712f746cc4abbff643e90d7200b2

元コミット内容

commit 4925f8aa79db712f746cc4abbff643e90d7200b2
Author: Carl Shapiro <cshapiro@google.com>
Date:   Fri Apr 12 16:04:19 2013 -0700

    test: make goprint.go wait longer for go its routine to execute
    
    Update #5281
    
    R=golang-dev, r, bradfitz, cshapiro
    CC=golang-dev
    https://golang.org/cl/8631047

変更の背景

この変更は、Go言語のIssue #5281 に関連しています。このIssueは、test/goprint.go テストが時折失敗するという報告に対応するものです。テストの失敗は、main 関数内で起動されたゴルーチンが、その println 処理を完了する前に main 関数が終了してしまうことによって引き起こされる競合状態が原因でした。

Goのテスト環境や実行環境によっては、ゴルーチンのスケジューリングが異なるため、短い time.Sleep ではゴルーチンが実行される保証がありませんでした。特に、テストが実行される環境の負荷やCPUの利用状況によって、ゴルーチンの実行が遅延し、main 関数がゴルーチンの完了を待たずに終了してしまうことがありました。この不安定な挙動を解消し、テストの信頼性を向上させるために、ゴルーチンに十分な実行時間を与える目的で time.Sleep の時間を延長する必要がありました。

前提知識の解説

Go言語のゴルーチン (Goroutines)

Go言語は、並行処理を言語レベルでサポートしており、その中心的な機能が「ゴルーチン」です。ゴルーチンは、軽量なスレッドのようなもので、Goランタイムによって管理されます。go キーワードを関数の呼び出しの前に置くことで、その関数は新しいゴルーチンとして並行して実行されます。

  • 軽量性: OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に起動してもオーバーヘッドが少ないです。
  • スケジューリング: GoランタイムのスケジューラがゴルーチンをOSスレッドにマッピングし、効率的に実行を切り替えます。
  • 非同期実行: ゴルーチンは非同期に実行されるため、親ゴルーチン(この場合はmain関数)は子ゴルーチンの完了を待たずに次の処理に進みます。

time.Sleep

time.Sleep 関数は、指定された期間だけ現在のゴルーチンの実行を一時停止させます。これは、特定の処理が完了するのを待つための単純なメカニズムとして使用されることがありますが、厳密な同期には適していません。なぜなら、Sleep は「少なくともこの期間だけ待つ」ことを保証するものであり、「指定された期間の後に他のゴルーチンが完了している」ことを保証するものではないからです。特に、テストのような厳密なタイミングが求められるシナリオでは、Sleep の使用は競合状態を引き起こす可能性があります。

競合状態 (Race Condition)

競合状態とは、複数のゴルーチン(またはスレッド)が共有リソースにアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。今回のケースでは、main ゴルーチンと println を実行するゴルーチンが競合していました。main ゴルーチンが time.Sleep の後に終了してしまうと、println ゴルーチンがその出力を完了する前にプログラム全体が終了してしまい、テストが期待する出力を得られずに失敗するという問題が発生していました。

Goのテスト

Go言語には、標準ライブラリにテストフレームワークが組み込まれています。テストファイルは通常、テスト対象のファイルと同じディレクトリに _test.go というサフィックスを付けて配置されます。テスト関数は Test で始まり、*testing.T 型の引数を取ります。go test コマンドで実行されます。

技術的詳細

test/goprint.go は、Goの println 関数が様々な型の値を正しく出力できるかを検証するためのテストプログラムです。このプログラムの main 関数は、新しいゴルーチンを起動して println を呼び出し、その後 time.Sleep で一時停止します。

元のコードでは time.Sleep(1e6) となっていました。Goの time パッケージでは、期間は time.Duration 型で表され、1e61 * 10^6 ナノ秒、つまり 1 ミリ秒 (1ms) を意味します。

func main() {
	go println(42, true, false, true, 1.5, "world", (chan int)(nil), []int(nil), (map[string]int)(nil), (func())(nil), byte(255))
	time.Sleep(1e6) // 1ミリ秒待機
}

この1ミリ秒という待機時間は、println を実行するゴルーチンが確実に完了するには短すぎることがありました。特に、システムがビジーな場合や、ゴルーチンのスケジューリングにわずかな遅延が生じた場合、main 関数が println ゴルーチンの完了を待たずに終了してしまい、テストが失敗する原因となっていました。これは典型的な競合状態であり、テストの非決定的な失敗(flaky test)につながります。

新しいコードでは time.Sleep(100*time.Millisecond) に変更されました。これは 100 ミリ秒、つまり 0.1 秒の待機を意味します。

func main() {
	go println(42, true, false, true, 1.5, "world", (chan int)(nil), []int(nil), (map[string]int)(nil), (func())(nil), byte(255))
	time.Sleep(100*time.Millisecond) // 100ミリ秒待機
}

この変更により、println ゴルーチンがその処理を完了するための十分な時間が確保され、テストの安定性が大幅に向上しました。100ms は、ほとんどの実行環境において、単純な println 処理が完了するには十分な時間であり、テストの信頼性を高めるための実用的な解決策です。

ただし、time.Sleep を用いた同期は、厳密な同期メカニズムではありません。より堅牢な並行処理の同期には、Goのチャネル(chan)や sync パッケージ(sync.WaitGroup など)を使用するのが一般的です。しかし、このテストの目的は println の動作確認であり、複雑な同期メカニズムを導入するよりも、単純に待機時間を延長する方が変更の範囲が小さく、意図が明確であると判断されたと考えられます。

コアとなるコードの変更箇所

変更は test/goprint.go ファイルの main 関数内の一行です。

--- a/test/goprint.go
+++ b/test/goprint.go
@@ -12,5 +12,5 @@ import "time"

 func main() {
 	go println(42, true, false, true, 1.5, "world", (chan int)(nil), []int(nil), (map[string]int)(nil), (func())(nil), byte(255))
-	time.Sleep(1e6)
+	time.Sleep(100*time.Millisecond)
 }

コアとなるコードの解説

変更された行は time.Sleep の呼び出しです。

  • - time.Sleep(1e6): 変更前のコードでは、1e6 ナノ秒、つまり1ミリ秒だけメインゴルーチンを一時停止していました。これは、println を実行するゴルーチンが完了する前に main 関数が終了してしまう可能性があり、テストの不安定性の原因となっていました。
  • + time.Sleep(100*time.Millisecond): 変更後のコードでは、100*time.Millisecond、つまり100ミリ秒だけメインゴルーチンを一時停止します。これにより、println ゴルーチンがその処理を完了するための十分な時間が確保され、テストが安定して実行されるようになります。

この変更は、Goのテストスイートにおける特定のテストの信頼性を向上させるための、シンプルかつ効果的な修正です。

関連リンク

参考にした情報源リンク