[インデックス 15628] ファイルの概要
このコミットは、GoランタイムにおけるWindows環境でのCGOコールバックに関する問題を修正するものです。具体的には、GoのCGO(C言語との相互運用機能)を利用した際に、Windows上でのコールバックが正しく動作しない、または不安定になるという問題(Issue #4955)に対処しています。この修正により、GoランタイムがWindowsのスレッドスケジューリングとより適切に連携し、CGOコールバックの信頼性が向上します。
コミット
commit 8aafb44b0bbba85535feb67e7ae0f4f254524c0f
Author: Russ Cox <rsc@golang.org>
Date: Thu Mar 7 09:18:48 2013 -0500
runtime: fix cgo callbacks on windows
Fixes #4955.
R=golang-dev, alex.brainman
CC=golang-dev
https://golang.org/cl/7563043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8aafb44b0bbba85535feb67e7ae0f4f25454c0f
元コミット内容
runtime: fix cgo callbacks on windows
Fixes #4955.
R=golang-dev, alex.brainman
CC=golang-dev
https://golang.org/cl/7563043
変更の背景
Go言語のCGO機能は、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数をコールバックとして呼び出したりすることを可能にします。しかし、Windows環境において、CGOを介したGo関数へのコールバックが不安定になる、または期待通りに動作しないという問題(Issue #4955)が存在していました。
この問題の根本原因は、Goランタイムがスレッドの「yield」(他のスレッドにCPUの実行権を譲る操作)や短いスリープを行う際に使用していたWindows API Sleep(0) にありました。Sleep(0) は、現在のスレッドの実行を一時停止し、同じ優先順位の他の準備完了スレッドにCPUを譲ることをOSに要求しますが、その挙動はOSのスケジューリングに大きく依存し、必ずしも即座に他のスレッドに切り替わるとは限りません。特に、CGOコールバックのような低レイテンシが求められるシナリオでは、この挙動が問題を引き起こす可能性がありました。
このコミットは、Sleep(0) の代わりに、より低レベルで制御性の高いWindows NT APIである NtWaitForSingleObject を使用することで、この問題を解決しようとしています。これにより、GoランタイムがWindowsのスレッドスケジューリングとより密接に連携し、CGOコールバックの信頼性とパフォーマンスを向上させることを目指しています。
前提知識の解説
CGO (C Foreign Function Interface for Go)
CGOは、GoプログラムがC言語の関数を呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。これにより、既存のCライブラリをGoから利用したり、パフォーマンスが重要な部分をCで記述したりすることが可能になります。CGOを使用すると、GoとCの間のデータ変換やスタック管理など、複雑な相互運用がGoツールチェーンによって自動的に処理されます。
Windows API: Sleep(0) と NtWaitForSingleObject
-
Sleep(0)(kernel32.dll):Sleep関数は、指定されたミリ秒数の間、現在のスレッドの実行を一時停止します。引数に0を指定した場合、スレッドは実行を一時停止し、同じ優先順位の他の準備完了スレッドにCPUを譲ることをOSに要求します。しかし、これはあくまで「要求」であり、OSのスケジューラがすぐに他のスレッドに切り替えることを保証するものではありません。場合によっては、すぐに同じスレッドが再開されることもあります。これは、主にCPUを他のスレッドに譲るためのヒントとして使用されます。 -
NtWaitForSingleObject(ntdll.dll):NtWaitForSingleObjectは、Windows NTカーネルのネイティブAPIであり、より低レベルの待機操作を提供します。この関数は、指定されたオブジェクトがシグナル状態になるか、タイムアウト期間が経過するまでスレッドを待機させます。このコミットでは、タイムアウト期間に負の値を指定することで、相対的な時間(この場合は短い時間)待機するように使用されています。特に、NtWaitForSingleObjectをタイムアウト値0で使用することは、Sleep(0)と同様にスレッドを即座に再スケジューリングさせる効果がありますが、より直接的にカーネルに働きかけるため、Sleep(0)よりも確実なスレッドのyieldが期待できます。
スレッドのYieldとスケジューリング
マルチタスクOSでは、CPUの実行時間は複数のスレッド間で共有されます。スレッドスケジューラは、どのスレッドがいつCPUを実行するかを決定します。スレッドが「yield」するということは、自発的にCPUの実行権をOSに返し、他のスレッドが実行される機会を与えることです。これは、特にCPUを大量に消費するスレッドが他のスレッドの進行を妨げないようにするために重要です。CGOコールバックの文脈では、GoランタイムがCコードからGo関数に切り替える際に、適切なタイミングでスレッドをyieldし、Goランタイムがコールバックを処理するための準備を整えることが重要になります。
技術的詳細
このコミットの主要な変更点は、GoランタイムがWindows上でスレッドのyield(runtime·osyield)と短いスリープ(runtime·usleep)を行う際に、従来の Sleep(0) APIの使用を廃止し、代わりに NtWaitForSingleObject を利用するように変更したことです。
Sleep(0) の問題点
Goランタイムは、内部的にスレッドの協調的なスケジューリングのために osyield や usleep のような関数を使用します。Windows環境では、これらが Sleep(0) を介して実装されていました。しかし、Sleep(0) はOSのスケジューラに依存する度合いが高く、特にCGOコールバックのような、CコードからGoコードへの迅速なコンテキスト切り替えが求められる場面で、期待通りの挙動をしないことがありました。これにより、コールバックが遅延したり、デッドロックのような問題が発生したりする可能性がありました。
NtWaitForSingleObject への移行
NtWaitForSingleObject は、kernel32.dll を介して提供される高レベルな Sleep 関数とは異なり、ntdll.dll を介して提供される低レベルなシステムコールラッパーです。この関数をタイムアウト値に負の値を指定して呼び出すことで、相対的な時間(この場合は非常に短い時間)待機させることができます。
-
runtime·osyieldの実装:runtime·osyieldでは、NtWaitForSingleObjectを呼び出し、タイムアウト値として-1を指定しています。これは、無限に待機するのではなく、非常に短い時間(または即座に)スレッドを再スケジューリングさせる効果があります。Sleep(0)と比較して、より確実なスレッドのyieldが期待できます。 -
runtime·usleepの実装:runtime·usleepでは、引数で与えられたマイクロ秒(us)を100ナノ秒単位に変換し、その負の値をNtWaitForSingleObjectのタイムアウト値として渡しています。Windowsのタイムアウト値は、正の値が絶対時間(1601年1月1日からの100ナノ秒間隔の数)、負の値が相対時間(現在の時刻からの100ナノ秒間隔の数)を表します。これにより、指定されたマイクロ秒数だけスレッドを正確にスリープさせることが可能になります。
アセンブリコードの変更
この変更は、GoランタイムのWindows向けアセンブリコード(sys_windows_386.s と sys_windows_amd64.s)に直接反映されています。これらのファイルでは、runtime·osyield と runtime·usleep の新しい実装が追加され、NtWaitForSingleObject を呼び出すための適切なスタックフレームとレジスタ設定が行われています。
また、src/pkg/runtime/thread_windows.c では、NtWaitForSingleObject を動的にロードするための dynimport ディレクティブが追加され、古い runtime·osyield と runtime·usleep のC言語実装が削除されています。これにより、Goランタイムは実行時に ntdll.dll から NtWaitForSingleObject 関数を解決し、利用できるようになります。
この変更は、GoランタイムがWindowsの低レベルなスレッドスケジューリングメカニズムとより密接に連携することを可能にし、CGOコールバックの信頼性と安定性を大幅に向上させます。
コアとなるコードの変更箇所
このコミットでは、以下のファイルが変更されています。
-
misc/cgo/test/cthread.go:- Windows環境での
testCthreadのスキップロジックが削除されました。これは、このコミットによってCGOコールバックの問題が解決されたため、テストがWindowsでも実行可能になったことを示しています。 sum.i = 0の初期化が追加されています。
- Windows環境での
-
src/pkg/runtime/sys_windows_386.s:- 32ビットWindows (x86) 環境向けのアセンブリコードです。
runtime·osyield関数が追加され、NtWaitForSingleObjectを呼び出すように実装されました。runtime·usleep関数が追加され、NtWaitForSingleObjectを呼び出すように実装されました。checkstack4<>というヘルパー関数が追加されています。
-
src/pkg/runtime/sys_windows_amd64.s:- 64ビットWindows (AMD64) 環境向けのアセンブリコードです。
runtime·osyield関数が追加され、NtWaitForSingleObjectを呼び出すように実装されました。runtime·usleep関数が追加され、NtWaitForSingleObjectを呼び出すように実装されました。
-
src/pkg/runtime/thread_windows.c:- Windows固有のランタイムスレッド処理に関するC言語コードです。
#pragma dynimport runtime·NtWaitForSingleObject NtWaitForSingleObject "ntdll.dll"が追加され、NtWaitForSingleObject関数をntdll.dllから動的にインポートするように指定されました。extern void *runtime·NtWaitForSingleObject;が追加されました。- 既存の
runtime·osyieldおよびruntime·usleepのC言語実装が削除されました。これらの関数は、アセンブリコードで再実装されることになります。
コアとなるコードの解説
src/pkg/runtime/sys_windows_386.s および src/pkg/runtime/sys_windows_amd64.s の変更
これらのファイルでは、Goランタイムの runtime·osyield と runtime·usleep 関数がアセンブリ言語で再実装されています。
runtime·osyield (32-bit/64-bit共通の概念)
この関数は、スレッドがCPUの実行権を自発的にOSに譲るために使用されます。
新しい実装では、NtWaitForSingleObject を呼び出しています。
-
32-bit (sys_windows_386.s):
MOVL runtime·NtWaitForSingleObject(SB), AX:NtWaitForSingleObject関数のアドレスをAXレジスタにロードします。MOVL $-1, hi-4(SP): タイムアウト値の上位32ビットをスタックにプッシュします。-1は負の値を表すため、上位ビットも設定されます。MOVL $-1, lo-8(SP): タイムアウト値の下位32ビットをスタックにプッシュします。-1は負の値を表すため、下位ビットも設定されます。LEAL lo-8(SP), BX: タイムアウト値が格納されているスタックのアドレスをBXにロードします。MOVL BX, ptime-12(SP):NtWaitForSingleObjectのlpTimeout引数として、タイムアウト値のアドレスをスタックにプッシュします。MOVL $0, alertable-16(SP):bAlertable引数に0を設定します。MOVL $-1, handle-20(SP):hHandle引数にINVALID_HANDLE_VALUE(-1) を設定します。これは、特定のオブジェクトを待機するのではなく、単にタイムアウトを待機することを示します。CALL AX:NtWaitForSingleObject関数を呼び出します。
-
64-bit (sys_windows_amd64.s): 64ビット版では、レジスタ渡しが使用されるため、スタック操作が異なります。
MOVQ runtime·NtWaitForSingleObject(SB), AX:NtWaitForSingleObject関数のアドレスをAXレジスタにロードします。MOVQ $1, BX/NEGQ BX: タイムアウト値として-1をBXレジスタに設定します。MOVQ SP, R8: スタックポインタをR8にコピーし、lpTimeout引数として使用します。MOVQ BX, (R8):R8が指すスタック位置にタイムアウト値-1を書き込みます。MOVQ $-1, CX:hHandle引数に-1を設定します。MOVQ $0, DX:bAlertable引数に0を設定します。CALL AX:NtWaitForSingleObject関数を呼び出します。
runtime·usleep (32-bit/64-bit共通の概念)
この関数は、指定されたマイクロ秒数だけスレッドをスリープさせます。
-
32-bit (sys_windows_386.s):
MOVL usec+0(FP), BX: 引数usec(マイクロ秒) をBXレジスタにロードします。IMULL $10, BX:usecを10倍します。これは、マイクロ秒を100ナノ秒単位に変換するためです(1マイクロ秒 = 1000ナノ秒 = 10 * 100ナノ秒)。NEGL BX:BXの値を負にします。NtWaitForSingleObjectのタイムアウト引数は、相対時間を負の値で表します。- 残りの部分は
osyieldと同様に、NtWaitForSingleObjectを呼び出すためのスタック設定と呼び出しを行います。
-
64-bit (sys_windows_amd64.s):
MOVL usec+0(FP), BX: 引数usecをBXレジスタにロードします。IMULQ $10, BX:usecを10倍します。NEGQ BX:BXの値を負にします。- 残りの部分は
osyieldと同様に、NtWaitForSingleObjectを呼び出すためのレジスタ設定と呼び出しを行います。
src/pkg/runtime/thread_windows.c の変更
-
#pragma dynimport runtime·NtWaitForSingleObject NtWaitForSingleObject "ntdll.dll": このディレクティブは、GoランタイムがNtWaitForSingleObject関数をntdll.dllから動的にロードすることをコンパイラに指示します。これにより、Goプログラムが実行される際に、この関数がOSから利用可能になります。 -
extern void *runtime·NtWaitForSingleObject;: これは、runtime·NtWaitForSingleObjectという名前のポインタが外部で定義されていることを宣言しています。このポインタは、動的にロードされたNtWaitForSingleObject関数のアドレスを保持するために使用されます。 -
既存の
runtime·osyieldおよびruntime·usleepのC言語実装の削除: これらの関数は、アセンブリ言語でより低レベルかつ効率的に再実装されたため、C言語での実装は不要となり削除されました。
これらの変更により、GoランタイムはWindows上でのスレッドのyieldとスリープをより正確かつ信頼性の高い方法で制御できるようになり、CGOコールバックの安定性が向上しました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/8aafb44b0bbba85535feb67e7ae0f4f254524c0f
- Gerrit Change-ID: https://golang.org/cl/7563043
参考にした情報源リンク
- Go CGO Documentation: https://go.dev/blog/c-go-is-not-c (CGOの一般的な情報)
- Microsoft Docs - Sleep function: https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleep
- Microsoft Docs - NtWaitForSingleObject function: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntwaitforsingleobject (Windows NT Native APIに関する情報)
- Windows Time: https://learn.microsoft.com/en-us/windows/win32/sysinfo/windows-time (Windowsにおける時間表現に関する情報)
- Go Assembly Language: https://go.dev/doc/asm (Goのアセンブリ言語に関する情報)