[インデックス 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.After
や time.Tick
のようなタイマー関連の機能がストレス下で正しく動作するかを検証するものです。このテストでは、バックグラウンドのゴルーチンが runtime.Gosched()
を呼び出してCPUを他のゴルーチンに譲り、メインのゴルーチンがタイマーイベントを受け取ってテストを終了させることを期待していました。
しかし、windows-386
環境のような特定の条件下では、runtime.Gosched()
が期待通りにOSスケジューラに制御を戻さず、タイマーイベントを生成するOSのスレッドが十分に実行されないことがありました。これにより、メインのゴルーチンが stop
フラグを設定できず、テストが無限ループに陥り、ハングアップまたはタイムアウトする問題が発生していました。このコミットは、この不安定性を解消し、テストの信頼性を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と関連するシステムコールに関する知識が必要です。
-
ゴルーチン (Goroutine): Go言語における軽量な実行スレッドです。OSのスレッドよりもはるかに軽量で、数千から数百万のゴルーチンを同時に実行できます。Goランタイムのスケジューラによって管理されます。
-
Goスケジューラ: Goランタイムには独自のスケジューラが組み込まれており、複数のゴルーチンを少数のOSスレッドにマッピングして実行します。これにより、コンテキストスイッチのオーバーヘッドを最小限に抑えつつ、高い並行性を実現します。スケジューラは、ゴルーチンがI/O操作でブロックされたり、明示的にCPUを譲ったりする際に、他のゴルーチンに切り替えます。
-
runtime.Gosched()
: この関数は、現在のゴルーチンがCPUを自発的にGoスケジューラに譲ることを指示します。これにより、Goスケジューラは他の実行可能なゴルーチンにCPUを割り当てることができます。これは、協調的マルチタスクの一種であり、ゴルーチンが長時間CPUを占有するのを防ぎ、他のゴルーチンにも実行機会を与えるために使用されます。ただし、Gosched()
はOSスケジューラに直接制御を戻すわけではなく、Goスケジューラが次にどのゴルーチンを実行するかを決定します。 -
time.Sleep()
: この関数は、指定された期間だけ現在のゴルーチンの実行を一時停止します。time.Sleep()
は、Goスケジューラだけでなく、基盤となるOSスケジューラにも影響を与えます。指定された時間が経過するまで、現在のゴルーチンは実行可能な状態ではなくなり、OSは他のプロセスやスレッドにCPUを割り当てることができます。特に短い時間(例:Nanosecond
)であっても、OSレベルでのスケジューリングイベントを発生させる可能性があります。 -
time.Tick()
とtime.After()
:time
パッケージの関数で、時間ベースのイベントを扱うために使用されます。time.Tick(d)
: 指定された期間d
ごとに現在時刻を送信するチャネルを返します。time.After(d)
: 指定された期間d
が経過した後に現在時刻を一度だけ送信するチャネルを返します。 これらの関数は内部的にタイマーを使用し、OSのタイマー機能と連携して動作します。
-
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
フラグが適切に設定され、テストが正常に終了するようになります。
関連リンク
- Go CL (Change List): https://golang.org/cl/7128053
参考にした情報源リンク
- Go言語の公式ドキュメント:
runtime.Gosched()
: https://pkg.go.dev/runtime#Goschedtime.Sleep()
: https://pkg.go.dev/time#Sleeptime.Tick()
: https://pkg.go.dev/time#Ticksync/atomic
パッケージ: https://pkg.go.dev/sync/atomic
- Goスケジューラに関する一般的な情報源 (例: Goのスケジューラに関するブログ記事や公式ブログ):
- "Go's work-stealing scheduler": https://go.dev/blog/go115-godebug (Go 1.15の変更点だが、スケジューラの概念理解に役立つ)
- "The Go scheduler": https://go.dev/doc/articles/go_scheduler.html (古い記事だが、基本的な概念は変わらない)
- WindowsにおけるGoのスケジューリングに関する情報 (必要に応じて検索)
windows-386
環境特有の挙動に関する具体的な情報は見つけにくいが、OSスケジューラとの連携の難しさを示唆する一般的な議論は存在する。