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

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

このコミットは、GoランタイムにおけるWindows環境での動的優先度昇格(Dynamic Priority Boosting)を無効にする変更を導入しています。これにより、WindowsのスケジューリングメカニズムがGoの並行処理モデルと衝突し、パフォーマンス問題を引き起こす可能性を排除します。特に、I/O負荷の高いシナリオやユニプロセッサ環境でのタイマースレッドのスケジューリング遅延が改善されます。

コミット

commit a574822f8044204357236405dca60d1ca5123ab5
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Sun Aug 4 14:08:13 2013 +0400

    runtime: disable dynamic priority boosting on windows
    Windows dynamic priority boosting assumes that a process has different types
    of dedicated threads -- GUI, IO, computational, etc. Go processes use
    equivalent threads that all do a mix of GUI, IO, computations, etc.
    In such context dynamic priority boosting does nothing but harm, so turn it off.
    In particular, if 2 goroutines do heavy IO on a server uniprocessor machine,
    windows rejects to schedule timer thread for 2+ seconds when priority boosting is enabled.
    Fixes #5971.
    
    R=alex.brainman
    CC=golang-dev
    https://golang.org/cl/12406043

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

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

元コミット内容

runtime: disable dynamic priority boosting on windows
Windows dynamic priority boosting assumes that a process has different types
of dedicated threads -- GUI, IO, computational, etc. Go processes use
equivalent threads that all do a mix of GUI, IO, computations, etc.
In such context dynamic priority boosting does nothing but harm, so turn it off.
In particular, if 2 goroutines do heavy IO on a server uniprocessor machine,
windows rejects to schedule timer thread for 2+ seconds when priority boosting is enabled.
Fixes #5971.

変更の背景

この変更の背景には、Windowsオペレーティングシステムが持つスレッドスケジューリングの特性と、Go言語のランタイムがスレッドをどのように利用するかのミスマッチがあります。

Windowsのスケジューラは、通常、I/O完了やGUIイベントの発生など、特定のアクションを行ったスレッドの優先度を一時的に引き上げる「動的優先度昇格(Dynamic Priority Boosting)」というメカニズムを持っています。これは、ユーザーインタラクションやI/O処理の応答性を向上させることを目的としています。Windowsは、GUIスレッド、I/Oスレッド、計算スレッドなど、異なる種類の専用スレッドが存在することを前提としています。

しかし、Goランタイムは、これらのWindowsの前提とは異なる方法でスレッドを利用します。Goのランタイムは、少数のOSスレッド(M: Machine)上で多数のゴルーチン(G: Goroutine)を多重化して実行します。これらのOSスレッドは、GUI、I/O、計算など、あらゆる種類のタスクを混在して実行します。つまり、GoのOSスレッドは特定の役割に特化しているわけではなく、Goランタイムのスケジューラによって動的に様々なゴルーチンが割り当てられます。

このようなGoの設計思想とWindowsの動的優先度昇格が組み合わさると、予期せぬパフォーマンス問題が発生することが判明しました。特に、コミットメッセージに記載されているように、「2つのゴルーチンがサーバーのユニプロセッサマシンで大量のI/Oを行う場合、優先度昇格が有効になっていると、Windowsはタイマースレッドのスケジューリングを2秒以上拒否する」という問題が発生していました。これは、I/Oを行ったGoのOSスレッドの優先度が不必要に引き上げられ、他の重要なランタイムスレッド(例えば、タイマーを処理するスレッドや、他のゴルーチンをスケジューリングするスレッド)の実行が妨げられるためと考えられます。

この問題を解決し、GoアプリケーションがWindows上でより予測可能で効率的に動作するようにするために、動的優先度昇格を無効にする決定がなされました。これにより、GoランタイムがOSスレッドを均等に扱い、Goの内部スケジューリングがより効果的に機能するようになります。

前提知識の解説

1. Goランタイムとゴルーチン、OSスレッド

  • ゴルーチン (Goroutine): Go言語における軽量な並行実行単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムがゴルーチンのスケジューリングを管理します。
  • OSスレッド (Operating System Thread): オペレーティングシステムが管理する実行単位です。CPUコア上で実際にコードを実行するのはOSスレッドです。
  • Goランタイムのスケジューラ (M:Nスケジューリング): Goランタイムは、M個のゴルーチンをN個のOSスレッドに多重化して実行します(M:Nスケジューリングモデル)。これにより、OSスレッドの切り替えコストを最小限に抑えつつ、高い並行性を実現します。GoのOSスレッドは、特定のタスクに特化せず、I/O、計算、ネットワーク処理など、あらゆる種類のゴルーチンを実行します。

2. Windowsの動的優先度昇格 (Dynamic Priority Boosting)

Windowsのプロセスおよびスレッドスケジューラは、システムの応答性を向上させるために、スレッドの優先度を動的に調整するメカニズムを持っています。

  • 優先度クラスと相対優先度: Windowsのスレッドは、プロセス全体の「優先度クラス」(例: NORMAL_PRIORITY_CLASS, HIGH_PRIORITY_CLASS)と、そのクラス内での「相対優先度」(例: THREAD_PRIORITY_NORMAL, THREAD_PRIORITY_ABOVE_NORMAL)の組み合わせで優先度が決定されます。
  • 動的優先度昇格: 特定のイベント(例: I/O完了、GUIイベント、待機状態からの復帰)が発生すると、Windowsスケジューラは一時的にそのスレッドの動的優先度を引き上げます。これにより、そのスレッドがCPUをより早く獲得し、応答性が向上することが期待されます。この昇格は一時的なもので、スレッドがCPUを消費すると徐々に元の優先度に戻ります。
  • SetProcessPriorityBoost 関数: このWindows API関数は、プロセスのスレッドが動的優先度昇格を受けるかどうかを制御します。TRUE(デフォルト)に設定すると昇格が有効になり、FALSEに設定すると無効になります。

3. 問題の根源

GoのOSスレッドは、I/O処理を含む様々なゴルーチンを実行します。Windowsの動的優先度昇格が有効な場合、GoのOSスレッドがI/Oを完了するたびにその優先度が引き上げられます。しかし、GoランタイムはすべてのOSスレッドを同等に扱い、特定のOSスレッドが常に高い優先度を持つことを期待していません。むしろ、Goランタイムの内部スケジューラがゴルーチンを効率的にOSスレッドに割り当てることで、全体のパフォーマンスを最適化しようとします。

I/O完了によって特定のGoのOSスレッドの優先度が不必要に引き上げられると、以下のような問題が発生します。

  • スケジューリングの不均衡: 優先度が上がったスレッドがCPUを独占しやすくなり、他のGoのOSスレッド(例えば、タイマー処理やガベージコレクションなど、ランタイムの健全な動作に必要なタスクを実行するスレッド)がCPUを獲得しにくくなります。
  • レイテンシの増加: 特にユニプロセッサ環境やCPUリソースが限られている環境では、優先度の高いスレッドが他の重要なランタイムタスクの実行を妨げ、タイマーの遅延や全体的な応答性の低下を引き起こす可能性があります。コミットメッセージの例では、タイマースレッドのスケジューリングが2秒以上遅れるという深刻な問題が指摘されています。

このため、Goランタイムの設計思想とWindowsの動的優先度昇格のミスマッチが、Windows上でのGoアプリケーションのパフォーマンスと安定性に悪影響を与えていたのです。

技術的詳細

このコミットは、Windows環境におけるGoランタイムの動作を改善するために、主に以下の2つの技術的な変更を導入しています。

1. Windowsの動的優先度昇格の無効化

最も重要な変更は、Goプロセス全体に対してWindowsの動的優先度昇格を無効にすることです。これは、src/pkg/runtime/os_windows.c ファイル内の runtime·osinit 関数に以下のコードを追加することで実現されています。

	void *kernel32;
	void *SetProcessPriorityBoost;

	kernel32 = runtime·stdcall(runtime·LoadLibraryA, 1, "kernel32.dll");
	if(kernel32 != nil) {
		// Windows dynamic priority boosting assumes that a process has different types
		// of dedicated threads -- GUI, IO, computational, etc. Go processes use
		// equivalent threads that all do a mix of GUI, IO, computations, etc.
		// In such context dynamic priority boosting does nothing but harm, so we turn it off.
		SetProcessPriorityBoost = runtime·stdcall(runtime·GetProcAddress, 2, kernel32, "SetProcessPriorityBoost");
		if(SetProcessPriorityBoost != nil)  // supported since Windows XP
			runtime·stdcall(SetProcessPriorityBoost, 2, (uintptr)-1, (uintptr)1);
	}
  • LoadLibraryA("kernel32.dll"): まず、Windows API関数を呼び出すために必要な kernel32.dll ライブラリをロードします。これは、Windowsのコアシステム機能を提供するDLLです。
  • GetProcAddress(kernel32, "SetProcessPriorityBoost"): ロードした kernel32.dll から、SetProcessPriorityBoost 関数のアドレスを取得します。この関数はWindows XP以降でサポートされています。
  • SetProcessPriorityBoost((uintptr)-1, (uintptr)1): 取得した SetProcessPriorityBoost 関数を呼び出します。
    • 最初の引数 (uintptr)-1 は、現在のプロセス(GetCurrentProcess() の擬似ハンドル)を指します。
    • 2番目の引数 (uintptr)1 は、FALSE を意味します。SetProcessPriorityBoost の2番目の引数はブール値で、TRUE(0以外)で昇格を有効にし、FALSE(0)で無効にします。ここでは 1 を渡すことで、動的優先度昇格を無効にしています。
      • 補足: Windows APIのブール値は通常 0FALSE0以外TRUE です。しかし、SetProcessPriorityBoost のドキュメントを見ると、2番目の引数は DisableBoost という名前で、TRUE を渡すとブーストを無効にすると記載されています。したがって、1 を渡すことでブーストを無効にしています。

この変更により、Goプロセス内のすべてのスレッドは、I/O完了などによって動的に優先度が引き上げられることがなくなり、Goランタイムのスケジューラがより予測可能にスレッドを管理できるようになります。

2. netpoll_windows.c における wait パラメータの変更

src/pkg/runtime/netpoll_windows.c ファイルでは、ネットワークポーリングの待機時間に関する変更が行われています。

--- a/src/pkg/runtime/netpoll_windows.c
+++ b/src/pkg/runtime/netpoll_windows.c
@@ -78,8 +78,7 @@ retry:
 	qty = 0;
 	wait = INFINITE;
 	if(!block)
-		// TODO(brainman): should use 0 here instead, but scheduler hogs CPU
-		wait = 1;
+		wait = 0;
 	// TODO(brainman): Need a loop here to fetch all pending notifications
 	// (or at least a batch). Scheduler will behave better if is given
 	// a batch of newly runnable goroutines.
  • 以前のコードでは、blockfalse の場合(つまり、ブロックせずにポーリングする場合)、wait 変数に 1 が設定されていました。これは、ポーリング関数がごく短い時間だけ待機することを意味します。コメントには「0 を使うべきだが、スケジューラがCPUを占有する」とありました。
  • このコミットでは、wait の値が 0 に変更されました。これは、ポーリング関数が全く待機せず、すぐに結果を返すことを意味します。

この変更は、動的優先度昇格の無効化と合わせて、Goランタイムのスケジューラがより細かくCPUリソースを制御できるようにするためのものです。wait = 0 にすることで、ネットワークイベントがない場合にポーリングスレッドがすぐにCPUを解放し、他のゴルーチンやランタイムタスクにCPUを譲る機会が増えます。これにより、CPUの占有を防ぎ、全体的なスケジューリングの効率が向上します。

3. timeout_test.go からのWindowsスキップの削除

src/pkg/net/timeout_test.go ファイルでは、Windows環境でのテストスキップが削除されました。

--- a/src/pkg/net/timeout_test.go
+++ b/src/pkg/net/timeout_test.go
@@ -423,8 +423,6 @@ func testVariousDeadlines(t *testing.T, maxProcs int) {
 	switch runtime.GOOS {\n \tcase \"plan9\":\n \t\tt.Skipf(\"skipping test on %q\", runtime.GOOS)\n-\tcase \"windows\":\n-\t\tt.Skipf(\"skipping test on %q, see issue 5971\", runtime.GOOS)\n \t}\n \n \tdefer runtime.GOMAXPROCS(runtime.GOMAXPROCS(maxProcs))\
  • 以前は、Issue 5971に関連する問題のため、Windows上での特定のタイムアウトテストがスキップされていました。
  • 今回のコミットでIssue 5971が修正されたため、このスキップが不要となり削除されました。これにより、Windows環境でもこのテストが実行されるようになり、GoランタイムのWindowsサポートの品質が向上します。

これらの変更は、GoランタイムがWindows上でより効率的かつ安定して動作するための重要な改善です。特に、WindowsのOSスケジューリングの特性とGoの並行処理モデルとの間のミスマッチを解消することを目的としています。

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

このコミットにおけるコアとなるコードの変更箇所は以下の通りです。

  1. src/pkg/runtime/os_windows.cruntime·osinit 関数内: Windows API SetProcessPriorityBoost を呼び出して、プロセスの動的優先度昇格を無効にする部分。

    // ... (既存のコード) ...
    
    void
    runtime·osinit(void)
    {
    	void *kernel32;
    	void *SetProcessPriorityBoost;
    
    	// ... (既存のコード) ...
    
    	kernel32 = runtime·stdcall(runtime·LoadLibraryA, 1, "kernel32.dll");
    	if(kernel32 != nil) {
    		// Windows dynamic priority boosting assumes that a process has different types
    		// of dedicated threads -- GUI, IO, computational, etc. Go processes use
    		// equivalent threads that all do a mix of GUI, IO, computations, etc.
    		// In such context dynamic priority boosting does nothing but harm, so we turn it off.
    		SetProcessPriorityBoost = runtime·stdcall(runtime·GetProcAddress, 2, kernel32, "SetProcessPriorityBoost");
    		if(SetProcessPriorityBoost != nil)  // supported since Windows XP
    			runtime·stdcall(SetProcessPriorityBoost, 2, (uintptr)-1, (uintptr)1);
    	}
    }
    
    // ... (既存のコード) ...
    
  2. src/pkg/runtime/netpoll_windows.c のネットワークポーリングループ内: wait 変数の値を 1 から 0 に変更する部分。

    // ... (既存のコード) ...
    
    retry:
    	qty = 0;
    	wait = INFINITE;
    	if(!block)
    		// TODO(brainman): should use 0 here instead, but scheduler hogs CPU
    -		wait = 1;
    +		wait = 0;
    	// TODO(brainman): Need a loop here to fetch all pending notifications
    	// (or at least a batch). Scheduler will behave better if is given
    	// a batch of newly runnable goroutines.
    
    // ... (既存のコード) ...
    
  3. src/pkg/net/timeout_test.gotestVariousDeadlines 関数内: Windowsでのテストスキップを削除する部分。

    // ... (既存のコード) ...
    
    	switch runtime.GOOS {
    	case "plan9":
    		t.Skipf("skipping test on %q", runtime.GOOS)
    -	case "windows":
    -		t.Skipf("skipping test on %q, see issue 5971", runtime.GOOS)
    	}
    
    	defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(maxProcs))
    
    // ... (既存のコード) ...
    

コアとなるコードの解説

1. SetProcessPriorityBoost による動的優先度昇格の無効化

src/pkg/runtime/os_windows.cruntime·osinit 関数は、GoランタイムがWindows上で初期化される際に呼び出される重要な関数です。この関数内で、Windows APIの SetProcessPriorityBoost が呼び出されています。

  • runtime·LoadLibraryA("kernel32.dll"): これは、Windowsのシステムライブラリである kernel32.dll をロードするためのGoランタイム内部のヘルパー関数です。kernel32.dll には、プロセスやスレッドの管理、メモリ操作など、基本的なシステム機能を提供する多くのAPIが含まれています。
  • runtime·GetProcAddress(kernel32, "SetProcessPriorityBoost"): ロードした kernel32.dll から、特定の関数(この場合は SetProcessPriorityBoost)のアドレスを取得します。これにより、GoランタイムはC言語のコードから直接Windows APIを呼び出すことができるようになります。
  • runtime·stdcall(SetProcessPriorityBoost, 2, (uintptr)-1, (uintptr)1): 取得した SetProcessPriorityBoost 関数を呼び出します。
    • runtime·stdcall は、GoランタイムがWindowsの stdcall 呼び出し規約に従って関数を呼び出すための内部メカニズムです。
    • 最初の引数 SetProcessPriorityBoost は、呼び出す関数のポインタです。
    • 2番目の引数 2 は、続く引数の数を示します。
    • (uintptr)-1 は、現在のプロセスを指す擬似ハンドルです。Windows APIでは、GetCurrentProcess() 関数が返す値と同じ意味を持ちます。
    • (uintptr)1 は、DisableBoost パラメータに TRUE を渡すことを意味します。SetProcessPriorityBoost 関数のドキュメントによると、このパラメータが TRUE の場合、システムはプロセスのスレッドの優先度を動的に引き上げなくなります。これにより、Goプロセス内のすべてのスレッドに対して動的優先度昇格が無効になります。

この変更は、GoランタイムがWindowsのスケジューリングメカニズムに過度に影響されることなく、Go自身のスケジューラがゴルーチンをより効率的に管理できるようにするために不可欠です。特に、I/Oバウンドなゴルーチンが頻繁に優先度を上げ、他の重要なランタイムタスク(タイマー、GCなど)の実行を妨げる問題を解決します。

2. netpoll_windows.c における wait パラメータの変更

src/pkg/runtime/netpoll_windows.c は、Windows環境でのネットワークI/Oポーリング(イベント通知)を担当するGoランタイムのコンポーネントです。このファイル内の変更は、ポーリングの待機動作を調整します。

  • if(!block) wait = 0;: この行は、ネットワークポーリングがブロックしないモードで実行される場合に、待機時間 wait0 に設定します。
    • 以前は wait = 1 でした。これは、ポーリング関数が1ミリ秒だけ待機することを意味します。コメントには「0 を使うべきだが、スケジューラがCPUを占有する」とありました。これは、wait = 0 にすると、ネットワークイベントがない場合にポーリングスレッドがすぐにCPUを解放し、他のゴルーチンやランタイムタスクにCPUを譲る機会が増えるためです。
    • この変更は、動的優先度昇格の無効化と組み合わされることで、Goランタイムのスケジューラがより細かくCPUリソースを制御できるようになります。ポーリングスレッドが不必要にCPUを保持するのを防ぎ、他のゴルーチンがより迅速に実行される機会を増やします。これにより、特にI/O負荷の高い状況での全体的な応答性とスループットが向上することが期待されます。

3. timeout_test.go からのWindowsスキップの削除

src/pkg/net/timeout_test.go は、ネットワークタイムアウトに関連するテストケースを含むファイルです。

  • 以前のコードでは、runtime.GOOS"windows" の場合、t.Skipf を使用して特定のテストがスキップされていました。スキップの理由として「see issue 5971」と明記されており、これはWindowsの動的優先度昇格に関連する問題が原因でテストが失敗していたことを示唆しています。
  • 今回のコミットで、SetProcessPriorityBoost を使用して動的優先度昇格が無効化され、関連する問題が解決されたため、このスキップ行が削除されました。これにより、Windows環境でもこのタイムアウトテストが正常に実行されるようになり、GoランタイムのWindowsサポートの堅牢性が確認できるようになりました。

これらのコード変更は、GoランタイムがWindowsのOSスケジューリングの特性をより適切に考慮し、Goの並行処理モデルと調和させるための重要なステップです。これにより、Windows上でのGoアプリケーションのパフォーマンスと安定性が向上します。

関連リンク

参考にした情報源リンク

  • Windows API SetProcessPriorityBoost のドキュメント (Microsoft Learn): https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setprocesspriorityboost
  • Go言語のスケジューラに関する一般的な情報 (Go公式ドキュメントやブログ記事など)
  • Windowsのスケジューリングに関する一般的な情報 (Microsoftの技術ドキュメントなど)
  • GoのIssue 5971 (コミットメッセージに記載されているが、直接的な公開リポジトリでの参照は見つからなかったため、コミットメッセージの内容を主要な情報源とした)