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

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

このコミットは、Go言語の time パッケージにおけるタイマーのオーバーフローテスト (Overflow timer test) の安定性を向上させるための変更です。具体的には、テスト中にゴルーチンを切り替える(yieldする)方法を runtime.Gosched() から runtime.GC() へと変更することで、特定の条件下でのテストの失敗(Issue 6874)を修正しています。

コミット

commit d98b3a7ee5a88ddb328e2a96f41f1e3cec40ad80
Author: Dave Cheney <dave@cheney.net>
Date:   Sun Feb 2 16:05:07 2014 +1100

    time: use an alternative method of yielding during Overflow timer test
    
    Fixes #6874.
    
    Use runtime.GC() as a stronger version of runtime.Gosched() which tends to bias the running goroutine in an otherwise idle system. This appears to reduce the worst case number of spins from 600 down to 30 on my 2 core system under high load.
    
    LGTM=iant
    R=golang-codereviews, lucio.dere, iant, dvyukov
    CC=golang-codereviews
    https://golang.org/cl/56540046

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

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

元コミット内容

    time: use an alternative method of yielding during Overflow timer test
    
    Fixes #6874.
    
    Use runtime.GC() as a stronger version of runtime.Gosched() which tends to bias the running goroutine in an otherwise idle system. This appears to reduce the worst case number of spins from 600 down to 30 on my 2 core system under high load.

変更の背景

この変更は、Go Issue 6874「time パッケージのタイマーテストが、高負荷時に runtime timer stuck: overflow in addtimer エラーで失敗することがある」を修正するために行われました。

問題の根源は、time パッケージの内部テスト CheckRuntimeTimerOverflow にありました。このテストは、非常に長い期間(例えば、数年後)に設定されたタイマーが正しく動作するかどうかを検証するものです。テストのロジックでは、タイマーが発火するのを待つ間に runtime.Gosched() を呼び出して、他のゴルーチン(特にタイマーサービスゴルーチン)にCPUを譲るように試みていました。

しかし、Goスケジューラの特性として、アイドル状態のシステムでは、以前に実行されていたゴルーチンに対してバイアスがかかる傾向があります。つまり、runtime.Gosched() を呼び出しても、すぐに同じゴルーチンが再スケジュールされてしまう可能性がありました。特に、システムが高負荷状態にある場合、タイマーサービスゴルーチンがタイムリーに実行されず、設定されたタイムアウト期間を超えてしまうことがありました。これにより、テストが「runtime timer stuck: overflow in addtimer」というエラーで失敗していました。

このコミットは、runtime.Gosched() の代わりに runtime.GC() を使用することで、このスケジューリングの遅延を緩和し、テストの安定性を向上させることを目的としています。

前提知識の解説

Goスケジューラとゴルーチン (Goroutines)

Go言語は、軽量な並行処理の単位として「ゴルーチン」を提供します。ゴルーチンはOSのスレッドよりもはるかに軽量で、数百万個を同時に実行することも可能です。Goランタイムには、これらのゴルーチンをOSスレッドにマッピングし、効率的に実行を切り替える「スケジューラ」が組み込まれています。

runtime.Gosched()

runtime.Gosched() は、現在のゴルーチンの実行を一時停止し、他のゴルーチンにCPUを譲るようにスケジューラにヒントを与える関数です。これにより、現在のゴルーチンは実行キューの最後に移動し、他の準備ができたゴルーチンが実行される機会を得ます。しかし、前述の通り、アイドル状態や特定の条件下では、スケジューラが同じゴルーチンをすぐに再スケジュールする「バイアス」を持つことがあります。

runtime.GC()

runtime.GC() は、Goランタイムのガベージコレクタを手動で実行する関数です。ガベージコレクション(GC)は、不要になったメモリを自動的に解放するプロセスです。GCが実行される際には、通常、すべてのゴルーチンが一時停止され(Stop-the-Worldフェーズ)、GCが完了した後にゴルーチンが再開されます。この「Stop-the-World」の性質が、このコミットにおける runtime.Gosched() の代替手段として重要になります。

Goのタイマー

Goの time パッケージは、時間ベースのイベントを扱うための機能を提供します。time.Aftertime.NewTimer などを使用して、指定された時間が経過した後にイベントを発生させることができます。これらのタイマーは、Goランタイム内部のタイマーサービスゴルーチンによって管理されています。このゴルーチンは、設定されたすべてのタイマーを監視し、期限が来たタイマーを発火させる役割を担っています。

技術的詳細

このコミットの核心は、runtime.Gosched() の代わりに runtime.GC() を使用することで、Goスケジューラの「以前に実行されていたゴルーチンへのバイアス」を克服し、タイマーサービスゴルーチンがより確実にスケジュールされるようにすることです。

runtime.Gosched() は、現在のゴルーチンを単に実行キューの最後に移動させるだけです。もしシステムがアイドル状態であったり、他の実行可能なゴルーチンが少なかったりする場合、スケジューラは効率を優先して、すぐに同じゴルーチンを再開する可能性があります。これは、特にタイマーテストのように、特定のゴルーチン(タイマーサービスゴルーチン)がタイムリーに実行されることが重要なシナリオでは問題となります。

一方、runtime.GC() を呼び出すと、Goランタイムはガベージコレクションプロセスを開始します。ガベージコレクションの実行中、特に「Stop-the-World」フェーズでは、すべてのゴルーチン(現在のテストゴルーチンとタイマーサービスゴルーチンを含む)が一時停止されます。GCが完了し、ゴルーチンが再開される際には、スケジューラは事実上「リセット」された状態となり、すべての準備ができたゴルーチンが公平にスケジュールされる機会を得ます。これにより、タイマーサービスゴルーチンがより迅速に実行され、タイマーの遅延が大幅に減少します。

コミットメッセージによると、この変更により、高負荷な2コアシステムでの最悪ケースのスピン回数(タイマーが発火するのを待つループの回数)が600回から30回にまで減少したと報告されています。これは、タイマーサービスゴルーチンがはるかに迅速にスケジュールされるようになったことを示しています。

また、sleep_test.got.Skip("skipping in short mode, see issue 6874") が追加されています。これは、go test -short コマンドでテストを実行する際に、この TestOverflowRuntimeTimer テストをスキップするためのものです。このテストは、非常に長い期間のタイマーを扱うため、実行に時間がかかる可能性があり、CI/CD環境などでの高速なテスト実行には適さないためです。Issue 6874がこのテストの不安定性に関連しているため、その旨がコメントされています。

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

diff --git a/src/pkg/time/internal_test.go b/src/pkg/time/internal_test.go
index d9592954b2..4ba6d478de 100644
--- a/src/pkg/time/internal_test.go
+++ b/src/pkg/time/internal_test.go
@@ -78,7 +78,15 @@ func CheckRuntimeTimerOverflow() error {
 		if Now().After(stop) {
 			return errors.New("runtime timer stuck: overflow in addtimer")
 		}
-		runtime.Gosched()
+		// Issue 6874. This test previously called runtime.Gosched to try to yield
+		// to the goroutine servicing t, however the scheduler has a bias towards the
+		// previously running goroutine in an idle system. Combined with high load due
+		// to all CPUs busy running tests t's goroutine could be delayed beyond the
+		// timeout window.
+		//
+		// Calling runtime.GC() reduces the worst case lantency for scheduling t by 20x
+		// under the current Go 1.3 scheduler.
+		runtime.GC()
 	}
 }
diff --git a/src/pkg/time/sleep_test.go b/src/pkg/time/sleep_test.go
index 4687259509..23cb3daebb 100644
--- a/src/pkg/time/sleep_test.go
+++ b/src/pkg/time/sleep_test.go
@@ -398,6 +398,9 @@ func TestIssue5745(t *testing.T) {
 }
 
 func TestOverflowRuntimeTimer(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping in short mode, see issue 6874")
+	}
 	if err := CheckRuntimeTimerOverflow(); err != nil {
 		t.Fatalf(err.Error())
 	}

コアとなるコードの解説

src/pkg/time/internal_test.go の変更

CheckRuntimeTimerOverflow 関数内の無限ループにおいて、runtime.Gosched() の呼び出しが runtime.GC() に置き換えられました。

変更前:

		runtime.Gosched()

変更後:

		// Issue 6874. This test previously called runtime.Gosched to try to yield
		// to the goroutine servicing t, however the scheduler has a bias towards the
		// previously running goroutine in an idle system. Combined with high load due
		// to all CPUs busy running tests t's goroutine could be delayed beyond the
		// timeout window.
		//
		// Calling runtime.GC() reduces the worst case lantency for scheduling t by 20x
		// under the current Go 1.3 scheduler.
		runtime.GC()

追加されたコメントは、この変更の理由を明確に説明しています。runtime.Gosched() がアイドルシステムでのスケジューラのバイアスにより、タイマーサービスゴルーチンへの譲渡が不十分であったこと、そして runtime.GC() がその問題を解決し、Go 1.3スケジューラの下でスケジューリングの最悪ケースの遅延を20倍削減したことを述べています。

src/pkg/time/sleep_test.go の変更

TestOverflowRuntimeTimer 関数に、testing.Short() モードでのスキップロジックが追加されました。

変更後:

func TestOverflowRuntimeTimer(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping in short mode, see issue 6874")
	}
	if err := CheckRuntimeTimerOverflow(); err != nil {
		t.Fatalf(err.Error())
	}
}

testing.Short() は、テストが短時間で実行されるべきかどうかを示すフラグです。このテストは実行に時間がかかる可能性があるため、go test -short コマンドで実行された場合にはスキップされるようになりました。これにより、開発者が迅速なフィードバックを得たい場合に、この時間のかかるテストを省略できるようになります。コメントには、このスキップがIssue 6874に関連していることが明記されています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (runtimeパッケージ、timeパッケージ)
  • Goスケジューラに関する一般的な情報源 (ブログ記事、Goのソースコード解説など)
  • ガベージコレクションに関する一般的な情報源 (GoのGCの仕組みなど)