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

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

このコミットは、GoランタイムがCgoコールバックを非Goスレッドから安全に処理できるようにするための重要な変更を導入しています。これにより、Goランタイムが直接作成していないCスレッドからGo関数を呼び出す際の制限が解消され、Cgoの相互運用性が大幅に向上しました。

コミット

commit 6c976393aea607e67f4d31e3a2ae7b3c0dc15ade
Author: Russ Cox <rsc@golang.org>
Date:   Wed Feb 20 17:48:23 2013 -0500

    runtime: allow cgo callbacks on non-Go threads
    
    Fixes #4435.
    
    R=golang-dev, iant, alex.brainman, minux.ma, dvyukov
    CC=golang-dev
    https://golang.org/cl/7304104

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

https://github.com/golang/go/commit/6c976393aea607e67f4d31e3a2ae7b3c0dc15ade

元コミット内容

このコミットは、GoランタイムがGoによって作成されていないスレッドからのCgoコールバックを許可するように修正します。これはIssue #4435を解決します。

変更の背景

Go言語はC言語との相互運用性を提供するためにCgoというメカニズムを持っています。これにより、GoコードからC関数を呼び出したり、CコードからGo関数を呼び出したりすることが可能です。しかし、このコミット以前は、Goランタイムが管理していない(つまり、Goがgoキーワードで作成したゴルーチンに対応するOSスレッドではない)CスレッドからGo関数をコールバックする際に問題がありました。

具体的には、Goランタイムは、Goのスケジューラが管理するM(Machine、OSスレッド)とG(Goroutine、ゴルーチン)のペアが常に存在することを前提として動作します。Cgoコールバックが非Goスレッドから行われた場合、そのスレッドには対応するMが存在しないため、Goランタイムがクラッシュしたり、未定義の動作を引き起こしたりする可能性がありました。

Issue #4435は、このCgoコールバックの制限を明確にし、非Goスレッドからのコールバックを安全に処理できるようにするための解決策を求めていました。この問題は、既存のCライブラリが独自のワーカースレッドプールを持ち、そこからGoのコールバック関数を呼び出したい場合に特に顕著でした。以前は、このようなシナリオでは、CスレッドからGoのゴルーチンにメッセージを渡し、そのゴルーチンが実際のGoコールバックを実行するというような、複雑な回避策が必要でした。

このコミットは、Goランタイムが非Goスレッドからのコールバックを検出し、一時的にそのスレッドにM(OSスレッドに対応するGoランタイムの構造体)を割り当てることで、この問題を解決します。これにより、Goランタイムの整合性を保ちつつ、Cgoの柔軟性が向上しました。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念とCgoの仕組みについて理解しておく必要があります。

  1. GoランタイムのM-G-Pモデル:

    • G (Goroutine): Goにおける軽量な実行単位。数KBのスタックを持ち、数百万個作成しても問題ありません。Goスケジューラによって管理されます。
    • M (Machine): OSスレッドに対応するGoランタイムの構造体。Gを実行するためのコンテキストを提供します。MはOSによって管理されるスレッドであり、GoランタイムはMをOSスレッドにマッピングします。
    • P (Processor): 論理プロセッサ。GをM上で実行するためのコンテキストを提供します。PはMとGの間に位置し、Gの実行に必要なリソース(スケジューラキュー、メモリ割り当てなど)を管理します。Goスケジューラは、Pの数に基づいてGをMにディスパッチします。
    • 通常、GoのコードはG上で実行され、GはPにアタッチされたM上で実行されます。Goランタイムは、必要に応じてMをOSに要求したり、解放したりします。
  2. Cgo:

    • GoプログラムからCコードを呼び出したり、CコードからGoプログラムを呼び出したりするためのGoの機能です。
    • GoからCを呼び出す場合(C.func()):Goランタイムは現在のMをCコードに引き渡し、Cコードが実行されます。Cコードの実行中は、そのMはGoスケジューラから切り離され、GoのGはブロックされます。
    • CからGoを呼び出す場合(//export GoFuncでエクスポートされたGo関数をCから呼び出す):これがこのコミットの焦点です。CコードがGo関数を呼び出す際、Goランタイムは呼び出し元のスレッドがGoによって管理されているMであるかどうかをチェックします。
  3. スレッドローカルストレージ (TLS):

    • 各スレッドが独自のデータを保持できるメモリ領域です。Goランタイムは、現在のMとGへのポインタをTLSに保存しています。これにより、Goコードは常に現在の実行コンテキスト(どのMとGで実行されているか)を迅速に参照できます。
  4. アトミック操作:

    • 複数のスレッドから同時にアクセスされても、その操作が中断されずに完全に実行されることを保証する操作です。ロックを使用せずに共有データを安全に操作するために使用されます。compare-and-swap (CAS)などが代表的です。

技術的詳細

このコミットの核心は、Goランタイムが管理していないOSスレッド(非Goスレッド)からGo関数がコールバックされた際に、そのスレッドに一時的にGoランタイムのM(Machine)を割り当て、Goコードが安全に実行できるようにするメカニズムです。

主な変更点と導入された概念は以下の通りです。

  1. runtime·cgocallbackの変更:

    • runtime·cgocallbackは、CコードからGo関数が呼び出されたときにGoランタイムが最初に実行するアセンブリ関数です。
    • このコミットでは、cgocallbackの冒頭で現在のスレッドにMが関連付けられているか(TLSのmがnilでないか)をチェックするロジックが追加されました。
    • もしMがnil(非Goスレッドからのコールバック)であれば、runtime·needm関数が呼び出されます。
  2. runtime·needm関数:

    • この関数は、非GoスレッドからのCgoコールバック時に、一時的に使用するMを取得するために導入されました。
    • needmは、Goランタイムが事前に確保しておいた「余分なM(extra M)」のリストからMを借用します。
    • このリストは、アトミック操作(runtime·atomicloadpruntime·casp)を使用してロックされ、Mの取得と解放がスレッドセーフに行われます。MLOCKEDという特殊な値(((M*)1))を使って、リストがロックされている状態を示します。
    • Mを取得した後、needmはそのMを現在のスレッドのTLSに設定し、そのMのg0(スケジューリングスタック)とcurg(現在のゴルーチン)を初期化します。これにより、Goコードが実行できる状態になります。
    • needmは、もし余分なMのリストが空になった場合、mp->needextramフラグを設定します。これは、後でruntime·newextramを呼び出して新しいMをリストに追加する必要があることを示します。
  3. runtime·dropm関数:

    • Cgoコールバックが完了し、GoコードからCコードに戻る際に呼び出されます。
    • dropmは、needmで借用したMを余分なMのリストに戻します。
    • これにより、Mは再利用可能になり、リソースのリークを防ぎます。
    • dropmもまた、アトミック操作を使用してリストのロックとアンロックを行います。
  4. runtime·newextram関数:

    • 余分なMのリストが空になった場合に、新しいMを割り当ててリストに追加するために導入されました。
    • この関数は、通常のGoランタイムのコンテキスト(MとGが適切に設定されている状態)で呼び出されるため、メモリ割り当てなどの通常のランタイム操作が可能です。
    • 新しいMには、runtime·goexitsched.pcとするダミーのG(ゴルーチン)が割り当てられます。これは、トレースバック時にゴルーチンのスタックの終わりを示すためです。
  5. runtime·setmg関数:

    • 現在のスレッドのTLSにMとGを設定するためのアセンブリ関数です。needmdropmから呼び出されます。
  6. runtime·allocmの導入とruntime·newmの変更:

    • runtime·newmは、OSスレッドを起動し、そのスレッドにMを関連付ける役割を担っていましたが、このコミットでruntime·allocmが導入され、Mの割り当てと初期化のロジックが分離されました。
    • allocmはM構造体を割り当て、g0(スケジューリングスタック)を初期化します。
    • newmallocmを呼び出してMを取得し、その後OS固有の関数(libcgo_thread_startruntime·newosproc)を呼び出してOSスレッドを起動し、そのスレッドでruntime·mstartを実行させます。
  7. テストケースの追加:

    • misc/cgo/test/cthread.gomisc/cgo/test/cthread_unix.cmisc/cgo/test/cthread_windows.cに新しいテストケースが追加されました。
    • これらのテストは、Cスレッドを複数作成し、それぞれのCスレッドからGo関数(Add)をコールバックすることで、新しいメカニズムが正しく機能するかどうかを検証します。特に、Go関数内でパニックが発生した場合でも、GoランタイムがクラッシュせずにCコードに制御が戻ることを確認しています。

この変更により、Goランタイムは、Goが管理していないスレッドからのCgoコールバックを、あたかもGoが作成したスレッドからのコールバックであるかのように安全に処理できるようになりました。これは、Goと既存のCライブラリとの連携をよりシームレスにし、Goの適用範囲を広げる上で非常に重要な改善です。

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

このコミットにおける主要なコード変更は、Goランタイムの以下のファイルに集中しています。

  • src/pkg/runtime/proc.c: M(Machine)の割り当て、管理、およびCgoコールバック時のMの取得・解放ロジック(needm, dropm, newextram, allocm, lockextra, unlockextra)が実装されています。
  • src/pkg/runtime/asm_386.s, src/pkg/runtime/asm_amd64.s, src/pkg/runtime/asm_arm.s: 各アーキテクチャのアセンブリコードで、runtime·cgocallback関数が修正され、非Goスレッドからのコールバック時にruntime·needmを呼び出すロジックが追加されました。また、runtime·setmg関数も追加されています。
  • src/pkg/runtime/runtime.h: 新しい関数(needm, dropm, setmg, newextram, unminit)のプロトタイプ宣言と、M構造体にneedextramフィールドが追加されています。
  • misc/cgo/test/cthread.go, misc/cgo/test/cthread_unix.c, misc/cgo/test/cthread_windows.c: 新しいCgoコールバックテストケースが追加されています。

コアとなるコードの解説

src/pkg/runtime/proc.c

このファイルには、Mの管理とCgoコールバック時のMの取得・解放に関する主要なロジックが含まれています。

  • M* runtime·extram;: グローバル変数として宣言された、余分なMのリストのヘッドポインタです。このリストは、非GoスレッドからのCgoコールバック時に一時的にMを借用するために使用されます。
  • #define MLOCKED ((M*)1): extramリストがロックされていることを示す特殊な値です。
  • static M* lockextra(bool nilokay):
    • extramリストをアトミックにロックし、リストのヘッドを返します。
    • runtime·atomicloadpで現在のextramの値を読み込みます。
    • もしextramMLOCKEDであれば、他のスレッドがロックしているため、runtime·osyield()を呼び出してCPUを譲り、再試行します。
    • nilokayfalseextramnilの場合(リストが空の場合)、runtime·usleep(1)で少し待ってから再試行します。これは、newextramが新しいMをリストに追加するのを待つためです。
    • runtime·casp(&runtime·extram, mp, MLOCKED)で、現在のextramの値がmp(読み込んだ値)と一致すれば、MLOCKEDに設定してロックを取得します。CASが失敗した場合は、他のスレッドが先に変更したため、再試行します。
  • static void unlockextra(M *mp):
    • extramリストのロックを解除し、新しいリストのヘッドをmpに設定します。
    • runtime·atomicstorep(&runtime·extram, mp)でアトミックに値を書き込みます。
  • void runtime·needm(byte x):
    • 非GoスレッドからのCgoコールバック時に呼び出され、一時的なMを取得します。
    • mp = lockextra(false);extramリストからMを借用します。falseを渡すことで、リストが空でないことを保証します。
    • mp->needextram = mp->schedlink == nil;:もし借用したMがリストの最後のMだった場合、needextramフラグを立てます。これは、後で新しいMをリストに追加する必要があることを示します。
    • unlockextra(mp->schedlink);で、借用したMの次の要素を新しいリストのヘッドとして設定し、ロックを解除します。
    • runtime·setmg(mp, mp->g0);で、現在のスレッドのTLSに借用したMと、そのMのg0(スケジューリングスタック)を設定します。
    • g->stackbaseg->stackguardを設定し、現在のCスレッドのスタックをGoランタイムが使用できるようにします。
    • Windowsの場合、SEH(Structured Exception Handling)フレームを設定します。
    • runtime·asminit()runtime·minit()を呼び出し、Mの初期化を行います。
  • void runtime·newextram(void):
    • 新しいMを割り当ててextramリストに追加します。
    • schedlock()schedunlock()でスケジューラをロックし、MとGの割り当てを保護します。
    • mp = runtime·allocm();で新しいMを割り当てます。
    • gp = runtime·malg(4096);で新しいG(ゴルーチン)を割り当て、そのsched.pcruntime·goexitに設定します。これは、このGがCgoコールバックのコンテキストとして使用され、Goコードの実行が終了した際にgoexitに到達することを示すためです。
    • mp->curg = gp;などでMとGを関連付けます。
    • mnext = lockextra(true);extramリストをロックし、mp->schedlink = mnext;で新しいMをリストの先頭に挿入します。
    • unlockextra(mp);でロックを解除します。
  • void runtime·dropm(void):
    • Cgoコールバックが終了し、GoコードからCコードに戻る際に呼び出され、借用したMをextramリストに戻します。
    • runtime·unminit();minitで行われた初期化を元に戻します。
    • runtime·setmg(nil, nil);で現在のスレッドのTLSからMとGをクリアします。
    • mnext = lockextra(true);extramリストをロックし、mp->schedlink = mnext;でMをリストの先頭に挿入します。
    • unlockextra(mp);でロックを解除します。
  • M* runtime·allocm(void):
    • M構造体を割り当て、そのg0(スケジューリングスタック)を初期化します。
    • この関数は、OSスレッドの起動とは独立してMを準備するために導入されました。

src/pkg/runtime/asm_*.s (アセンブリファイル)

各アーキテクチャのアセンブリファイル(asm_386.s, asm_amd64.s, asm_arm.s)には、runtime·cgocallbackruntime·setmgの変更が含まれています。

  • TEXT runtime·cgocallback(SB),7,$...:
    • この関数は、CコードからGo関数が呼び出されたときに実行されます。
    • 変更点として、TLSから現在のMを取得し、それがnil(Goが作成していないスレッド)である場合に、runtime·needmを呼び出すロジックが追加されました。
    • コールバックの終了時には、runtime·dropmを呼び出して借用したMを解放します。
  • TEXT runtime·setmg(SB), 7, $0:
    • mm(Mへのポインタ)とgg(Gへのポインタ)を引数に取り、現在のスレッドのTLSにそれらを設定します。
    • Windowsの場合、TLSの設定に加えて、FSレジスタ(386)またはGSレジスタ(amd64)に関連する処理も行われます。

これらの変更により、Goランタイムは非GoスレッドからのCgoコールバックを透過的に処理できるようになり、GoとCの相互運用性が大幅に向上しました。

関連リンク

参考にした情報源リンク