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

[インデックス 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つの主要コンポーネントから構成されています:

  1. G (Goroutine): 実行されるgoroutine。関数スタック、プログラムカウンタ、その他の実行状態を含む
  2. M (Machine/OS Thread): OSスレッド。実際にコードを実行するOSレベルの実行単位
  3. P (Processor): 論理プロセッサ。実行権限とリソースを管理する

runtime.Gosched()の役割

runtime.Gosched()は、現在のgoroutineを一時的に中断し、他のgoroutineが実行できるようにスケジューラに制御を譲る関数です。この関数は以下の動作を行います:

  1. 現在のgoroutineをグローバル実行キューに追加
  2. スケジューラが別のgoroutineを選択して実行
  3. 元のgoroutineは後で実行が再開される

協調的スケジューリング vs プリエンプティブスケジューリング

  • 協調的スケジューリング: goroutineが自発的に制御を譲る必要がある(2011年当時のGo)
  • プリエンプティブスケジューリング: ランタイムが強制的にgoroutineを中断できる(Go 1.14以降)

技術的詳細

問題の発生メカニズム

  1. メインgoroutine: テストの実行とstopフラグの設定を担当
  2. GCgoroutine: runtime.GC()を連続実行してメモリ圧迫をシミュレート
  3. 競合状態: GCgoroutineがruntime.GC()を頻繁に呼び出すことで、メインgoroutineが実行される機会を奪う

runtime.GC()の特性

runtime.GC()は以下の特性を持ちます:

  • Stop-the-world方式でガベージコレクションを実行
  • 実行中は他のgoroutineの実行を一時停止
  • CPUを集約的に使用
  • 完了後、呼び出し元のgoroutineが継続実行

修正の効果

runtime.Gosched()の追加により:

  1. GCgoroutineがruntime.GC()実行後に明示的にスケジューラに制御を譲る
  2. メインgoroutineがstopフラグを設定する機会を確保
  3. テストが正常に終了するようになる

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

修正箇所はsrc/pkg/time/sleep_test.goTestAfterStress関数内の無名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フラグを設定する機会を得られません。

テスト全体の流れ

  1. 初期化: stopフラグを0に設定
  2. GCgoroutine開始: バックグラウンドでGCを実行
  3. メイン処理: Tick(1)でタイマーをテスト
  4. 停止処理: atomic.StoreUint32(&stop, 1)でstopフラグを設定
  5. 終了: GCgoroutineが終了し、テストが完了

修正による改善点

  • デッドロック解消: GCgoroutineが無限ループに陥ることを防止
  • 公平性確保: メインgoroutineが実行される機会を保証
  • テストの信頼性向上: 一貫してテストが完了するようになる

関連リンク

参考にした情報源リンク