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

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

このコミットは、Go言語の標準ライブラリ time パッケージ内のテスト TestAfterStress において、runtime.Gosched() の代わりに time.Sleep(Nanosecond) を使用するように変更するものです。この変更は、特に低速な windows-386 ビルド環境でのテストの安定性向上を目的としています。runtime.Gosched() が期待通りにゴルーチンをスケジューリングせず、テストがハングアップする問題を time.Sleep() を用いることで解決しています。

コミット

  • コミットハッシュ: e0aa26a42719f8eae1cbf64349f3bee24a42e2a2
  • 作者: Alex Brainman alex.brainman@gmail.com
  • コミット日時: 2013年1月18日 金曜日 15:31:01 +1100

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

https://github.com/golang/go/commit/e0aa26a42719f8eae1cbf64349f3bee24a42e2a2

元コミット内容

time: Sleep does better job then runtime.Gosched in TestAfterStress

for slow windows-386 builder

R=golang-dev, dave, rsc
CC=golang-dev
https://golang.org/cl/7128053

変更の背景

この変更の背景には、Go言語のテストスイートが特定の環境、特に「低速な windows-386 ビルダー」で不安定になるという問題がありました。TestAfterStress というテストは、time.Aftertime.Tick のようなタイマー関連の機能がストレス下で正しく動作するかを検証するものです。このテストでは、バックグラウンドのゴルーチンが runtime.Gosched() を呼び出してCPUを他のゴルーチンに譲り、メインのゴルーチンがタイマーイベントを受け取ってテストを終了させることを期待していました。

しかし、windows-386 環境のような特定の条件下では、runtime.Gosched() が期待通りにOSスケジューラに制御を戻さず、タイマーイベントを生成するOSのスレッドが十分に実行されないことがありました。これにより、メインのゴルーチンが stop フラグを設定できず、テストが無限ループに陥り、ハングアップまたはタイムアウトする問題が発生していました。このコミットは、この不安定性を解消し、テストの信頼性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と関連するシステムコールに関する知識が必要です。

  1. ゴルーチン (Goroutine): Go言語における軽量な実行スレッドです。OSのスレッドよりもはるかに軽量で、数千から数百万のゴルーチンを同時に実行できます。Goランタイムのスケジューラによって管理されます。

  2. Goスケジューラ: Goランタイムには独自のスケジューラが組み込まれており、複数のゴルーチンを少数のOSスレッドにマッピングして実行します。これにより、コンテキストスイッチのオーバーヘッドを最小限に抑えつつ、高い並行性を実現します。スケジューラは、ゴルーチンがI/O操作でブロックされたり、明示的にCPUを譲ったりする際に、他のゴルーチンに切り替えます。

  3. runtime.Gosched(): この関数は、現在のゴルーチンがCPUを自発的にGoスケジューラに譲ることを指示します。これにより、Goスケジューラは他の実行可能なゴルーチンにCPUを割り当てることができます。これは、協調的マルチタスクの一種であり、ゴルーチンが長時間CPUを占有するのを防ぎ、他のゴルーチンにも実行機会を与えるために使用されます。ただし、Gosched() はOSスケジューラに直接制御を戻すわけではなく、Goスケジューラが次にどのゴルーチンを実行するかを決定します。

  4. time.Sleep(): この関数は、指定された期間だけ現在のゴルーチンの実行を一時停止します。time.Sleep() は、Goスケジューラだけでなく、基盤となるOSスケジューラにも影響を与えます。指定された時間が経過するまで、現在のゴルーチンは実行可能な状態ではなくなり、OSは他のプロセスやスレッドにCPUを割り当てることができます。特に短い時間(例: Nanosecond)であっても、OSレベルでのスケジューリングイベントを発生させる可能性があります。

  5. time.Tick()time.After(): time パッケージの関数で、時間ベースのイベントを扱うために使用されます。

    • time.Tick(d): 指定された期間 d ごとに現在時刻を送信するチャネルを返します。
    • time.After(d): 指定された期間 d が経過した後に現在時刻を一度だけ送信するチャネルを返します。 これらの関数は内部的にタイマーを使用し、OSのタイマー機能と連携して動作します。
  6. sync/atomic パッケージ: Go言語でアトミック操作(不可分操作)を行うためのパッケージです。複数のゴルーチンから共有変数に安全にアクセスするために使用されます。

    • atomic.LoadUint32(&stop): stop という uint32 型の変数の値をアトミックに読み込みます。これにより、他のゴルーチンによる同時書き込みがあっても、常に最新の整合性のある値が読み込まれることが保証されます。

技術的詳細

TestAfterStress テストでは、メインのゴルーチンと、タイマーイベントを待つバックグラウンドのゴルーチンが存在します。バックグラウンドのゴルーチンは、atomic.LoadUint32(&stop) == 0 の間ループし、runtime.GC() を呼び出した後、以前は runtime.Gosched() を呼び出していました。この Gosched() の目的は、CPUを譲り、メインのゴルーチンが time.Tick からのイベントを受け取り、最終的に stop フラグを 1 に設定できるようにすることでした。

問題は、特に windows-386 環境において、runtime.Gosched() がOSスケジューラに対して十分な「ヒント」を与えなかったことにあります。Goスケジューラは Gosched() が呼ばれると他のGoゴルーチンに切り替えることができますが、OSレベルでのタイマーイベントを生成するスレッド(GoランタイムがOSタイマーと連携するために使用するスレッド)が実行される保証はありませんでした。結果として、タイマーイベントがメインのゴルーチンに送信されず、stop フラグが更新されないため、バックグラウンドのゴルーチンが無限ループに陥り、テストがハングアップしていました。

このコミットでは、runtime.Gosched()time.Sleep(Nanosecond) に置き換えることでこの問題を解決しています。time.Sleep() は、たとえ Nanosecond という非常に短い期間であっても、GoランタイムにOSスケジューラに制御を戻す機会を与えます。これにより、OSはタイマーイベントを処理するスレッドを実行する機会を得て、time.Tick がチャネルにイベントを送信し、メインのゴルーチンが stop フラグを更新できるようになります。

runtime.Gosched() がGoスケジューラにのみ影響を与えるのに対し、time.Sleep() はOSレベルでのスリープを要求するため、より確実にOSスケジューラに制御を戻し、他のOSスレッド(この場合はタイマーを管理するスレッド)が実行される機会を創出します。この違いが、低速な環境でのテストの安定性向上に繋がりました。

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

変更は src/pkg/time/sleep_test.go ファイルの TestAfterStress 関数内で行われています。

--- a/src/pkg/time/sleep_test.go
+++ b/src/pkg/time/sleep_test.go
@@ -54,9 +54,10 @@ func TestAfterStress(t *testing.T) {
 	go func() {
 		for atomic.LoadUint32(&stop) == 0 {
 			runtime.GC()
-			// Need to yield, because otherwise
-			// the main goroutine will never set the stop flag.
-			runtime.Gosched()
+			// Yield so that the OS can wake up the timer thread,
+			// so that it can generate channel sends for the main goroutine,
+			// which will eventually set stop = 1 for us.
+			Sleep(Nanosecond)
 		}
 	}()
 	c := Tick(1)

コアとなるコードの解説

変更されたコードブロックは、TestAfterStress 関数内で起動されるバックグラウンドのゴルーチンです。

元のコード:

		for atomic.LoadUint32(&stop) == 0 {
			runtime.GC()
			// Need to yield, because otherwise
			// the main goroutine will never set the stop flag.
			runtime.Gosched()
		}

この部分では、stop フラグが 0 の間、無限ループで runtime.GC() を呼び出し、その後 runtime.Gosched() を呼び出してCPUを譲っていました。コメントにもあるように、これはメインのゴルーチンが stop フラグを設定できるようにするために必要でした。しかし、前述の通り、runtime.Gosched() が特定の環境で期待通りに機能しない問題がありました。

変更後のコード:

		for atomic.LoadUint32(&stop) == 0 {
			runtime.GC()
			// Yield so that the OS can wake up the timer thread,
			// so that it can generate channel sends for the main goroutine,
			// which will eventually set stop = 1 for us.
			Sleep(Nanosecond)
		}

runtime.Gosched()Sleep(Nanosecond) に置き換えられました。新しいコメントは、この変更の意図をより明確に説明しています。「OSがタイマースレッドを起動できるように譲る」という点が重要です。Sleep(Nanosecond) は、Goスケジューラだけでなく、OSスケジューラにも制御を戻す機会を与えるため、タイマーイベントがより確実に生成され、メインのゴルーチンに送信されるようになります。これにより、stop フラグが適切に設定され、テストが正常に終了するようになります。

関連リンク

参考にした情報源リンク