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

[インデックス 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ランタイムは、内部的にスレッドの協調的なスケジューリングのために osyieldusleep のような関数を使用します。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.ssys_windows_amd64.s)に直接反映されています。これらのファイルでは、runtime·osyieldruntime·usleep の新しい実装が追加され、NtWaitForSingleObject を呼び出すための適切なスタックフレームとレジスタ設定が行われています。

また、src/pkg/runtime/thread_windows.c では、NtWaitForSingleObject を動的にロードするための dynimport ディレクティブが追加され、古い runtime·osyieldruntime·usleep のC言語実装が削除されています。これにより、Goランタイムは実行時に ntdll.dll から NtWaitForSingleObject 関数を解決し、利用できるようになります。

この変更は、GoランタイムがWindowsの低レベルなスレッドスケジューリングメカニズムとより密接に連携することを可能にし、CGOコールバックの信頼性と安定性を大幅に向上させます。

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

このコミットでは、以下のファイルが変更されています。

  1. misc/cgo/test/cthread.go:

    • Windows環境での testCthread のスキップロジックが削除されました。これは、このコミットによってCGOコールバックの問題が解決されたため、テストがWindowsでも実行可能になったことを示しています。
    • sum.i = 0 の初期化が追加されています。
  2. src/pkg/runtime/sys_windows_386.s:

    • 32ビットWindows (x86) 環境向けのアセンブリコードです。
    • runtime·osyield 関数が追加され、NtWaitForSingleObject を呼び出すように実装されました。
    • runtime·usleep 関数が追加され、NtWaitForSingleObject を呼び出すように実装されました。
    • checkstack4<> というヘルパー関数が追加されています。
  3. src/pkg/runtime/sys_windows_amd64.s:

    • 64ビットWindows (AMD64) 環境向けのアセンブリコードです。
    • runtime·osyield 関数が追加され、NtWaitForSingleObject を呼び出すように実装されました。
    • runtime·usleep 関数が追加され、NtWaitForSingleObject を呼び出すように実装されました。
  4. 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·osyieldruntime·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): NtWaitForSingleObjectlpTimeout 引数として、タイムアウト値のアドレスをスタックにプッシュします。
    • 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: タイムアウト値として -1BX レジスタに設定します。
    • 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: usec10 倍します。これは、マイクロ秒を100ナノ秒単位に変換するためです(1マイクロ秒 = 1000ナノ秒 = 10 * 100ナノ秒)。
    • NEGL BX: BX の値を負にします。NtWaitForSingleObject のタイムアウト引数は、相対時間を負の値で表します。
    • 残りの部分は osyield と同様に、NtWaitForSingleObject を呼び出すためのスタック設定と呼び出しを行います。
  • 64-bit (sys_windows_amd64.s):

    • MOVL usec+0(FP), BX: 引数 usecBX レジスタにロードします。
    • IMULQ $10, BX: usec10 倍します。
    • 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コールバックの安定性が向上しました。

関連リンク

参考にした情報源リンク