[インデックス 10386] ファイルの概要
このコミットは、Go言語のtime
パッケージにおけるテストハングの修正を行ったものです。具体的には、src/pkg/time/sleep_test.go
ファイルのTestAfterStress
関数において、ガベージコレクションを実行するgoroutineにruntime.Gosched()
呼び出しを追加することで、競合状態を解決しました。
コミット
著者: Dmitriy Vyukov dvyukov@google.com
日付: Mon Nov 14 22:31:39 2011 +0300
コミットメッセージ: time: fix test hang
コミットハッシュ: ba98a7ee5eab17423674e8c85c5e694700dda61c
レビュー: R=golang-dev, bradfitz
変更行数: 1ファイル変更、3行追加
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ba98a7ee5eab17423674e8c85c5e694700dda61c
元コミット内容
diff --git a/src/pkg/time/sleep_test.go b/src/pkg/time/sleep_test.go
index dae7dfe8fb..4c4a079880 100644
--- a/src/pkg/time/sleep_test.go
+++ b/src/pkg/time/sleep_test.go
@@ -54,6 +54,9 @@ 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()
}
}()
c := Tick(1)
変更の背景
このコミットは、Go 1.0リリース前の重要なバグ修正として行われました。2011年当時、Go言語の並行処理システムは現在ほど成熟しておらず、goroutineのスケジューリングは主に協調的(cooperative)でした。この状況下で、CPUを集約的に使用するgoroutineが他のgoroutineの実行を妨げる問題が発生していました。
特に、TestAfterStress
テストでは、タイマー機能をストレステストするためにガベージコレクションを頻繁に実行するgoroutineが走っていましたが、このgoroutineがruntime.GC()
を連続して呼び出すことで、メインgoroutineがstop
フラグを設定する機会を奪っていました。その結果、テストが無限ループに陥り、ハングアップが発生していました。
前提知識の解説
Go言語のgoroutineスケジューラ
Go言語のgoroutineスケジューラは、以下の3つの主要コンポーネントから構成されています:
- G (Goroutine): 実行されるgoroutine。関数スタック、プログラムカウンタ、その他の実行状態を含む
- M (Machine/OS Thread): OSスレッド。実際にコードを実行するOSレベルの実行単位
- P (Processor): 論理プロセッサ。実行権限とリソースを管理する
runtime.Gosched()の役割
runtime.Gosched()
は、現在のgoroutineを一時的に中断し、他のgoroutineが実行できるようにスケジューラに制御を譲る関数です。この関数は以下の動作を行います:
- 現在のgoroutineをグローバル実行キューに追加
- スケジューラが別のgoroutineを選択して実行
- 元のgoroutineは後で実行が再開される
協調的スケジューリング vs プリエンプティブスケジューリング
- 協調的スケジューリング: goroutineが自発的に制御を譲る必要がある(2011年当時のGo)
- プリエンプティブスケジューリング: ランタイムが強制的にgoroutineを中断できる(Go 1.14以降)
技術的詳細
問題の発生メカニズム
- メインgoroutine: テストの実行とstopフラグの設定を担当
- GCgoroutine:
runtime.GC()
を連続実行してメモリ圧迫をシミュレート - 競合状態: GCgoroutineが
runtime.GC()
を頻繁に呼び出すことで、メインgoroutineが実行される機会を奪う
runtime.GC()の特性
runtime.GC()
は以下の特性を持ちます:
- Stop-the-world方式でガベージコレクションを実行
- 実行中は他のgoroutineの実行を一時停止
- CPUを集約的に使用
- 完了後、呼び出し元のgoroutineが継続実行
修正の効果
runtime.Gosched()
の追加により:
- GCgoroutineが
runtime.GC()
実行後に明示的にスケジューラに制御を譲る - メインgoroutineが
stop
フラグを設定する機会を確保 - テストが正常に終了するようになる
コアとなるコードの変更箇所
修正箇所はsrc/pkg/time/sleep_test.go
のTestAfterStress
関数内の無名goroutineです:
// 修正前
go func() {
for atomic.LoadUint32(&stop) == 0 {
runtime.GC()
}
}()
// 修正後
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()
}
}()
コアとなるコードの解説
atomic.LoadUint32(&stop)の役割
for atomic.LoadUint32(&stop) == 0 {
この部分では、アトミック操作を使用してstopフラグの状態を確認しています。atomic.LoadUint32
を使用することで、メモリ同期の問題を回避し、複数のgoroutine間でのデータ競合を防いでいます。
runtime.GC()の実行
runtime.GC()
この呼び出しは、ガベージコレクションを強制的に実行します。テストの目的は、タイマー機能をメモリ圧迫下でストレステストすることです。
runtime.Gosched()による制御の譲渡
// Need to yield, because otherwise
// the main goroutine will never set the stop flag.
runtime.Gosched()
この追加により、GCgoroutineが一度の反復後に明示的にスケジューラに制御を譲ります。コメントが明確に説明しているように、これがなければメインgoroutineがstop
フラグを設定する機会を得られません。
テスト全体の流れ
- 初期化:
stop
フラグを0に設定 - GCgoroutine開始: バックグラウンドでGCを実行
- メイン処理:
Tick(1)
でタイマーをテスト - 停止処理:
atomic.StoreUint32(&stop, 1)
でstopフラグを設定 - 終了: GCgoroutineが終了し、テストが完了
修正による改善点
- デッドロック解消: GCgoroutineが無限ループに陥ることを防止
- 公平性確保: メインgoroutineが実行される機会を保証
- テストの信頼性向上: 一貫してテストが完了するようになる