[インデックス 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の仕組みについて理解しておく必要があります。
-
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に要求したり、解放したりします。
-
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であるかどうかをチェックします。
-
スレッドローカルストレージ (TLS):
- 各スレッドが独自のデータを保持できるメモリ領域です。Goランタイムは、現在のMとGへのポインタをTLSに保存しています。これにより、Goコードは常に現在の実行コンテキスト(どのMとGで実行されているか)を迅速に参照できます。
-
アトミック操作:
- 複数のスレッドから同時にアクセスされても、その操作が中断されずに完全に実行されることを保証する操作です。ロックを使用せずに共有データを安全に操作するために使用されます。
compare-and-swap (CAS)
などが代表的です。
- 複数のスレッドから同時にアクセスされても、その操作が中断されずに完全に実行されることを保証する操作です。ロックを使用せずに共有データを安全に操作するために使用されます。
技術的詳細
このコミットの核心は、Goランタイムが管理していないOSスレッド(非Goスレッド)からGo関数がコールバックされた際に、そのスレッドに一時的にGoランタイムのM(Machine)を割り当て、Goコードが安全に実行できるようにするメカニズムです。
主な変更点と導入された概念は以下の通りです。
-
runtime·cgocallback
の変更:runtime·cgocallback
は、CコードからGo関数が呼び出されたときにGoランタイムが最初に実行するアセンブリ関数です。- このコミットでは、
cgocallback
の冒頭で現在のスレッドにMが関連付けられているか(TLSのm
がnilでないか)をチェックするロジックが追加されました。 - もしMがnil(非Goスレッドからのコールバック)であれば、
runtime·needm
関数が呼び出されます。
-
runtime·needm
関数:- この関数は、非GoスレッドからのCgoコールバック時に、一時的に使用するMを取得するために導入されました。
needm
は、Goランタイムが事前に確保しておいた「余分なM(extra M)」のリストからMを借用します。- このリストは、アトミック操作(
runtime·atomicloadp
とruntime·casp
)を使用してロックされ、Mの取得と解放がスレッドセーフに行われます。MLOCKED
という特殊な値(((M*)1)
)を使って、リストがロックされている状態を示します。 - Mを取得した後、
needm
はそのMを現在のスレッドのTLSに設定し、そのMのg0
(スケジューリングスタック)とcurg
(現在のゴルーチン)を初期化します。これにより、Goコードが実行できる状態になります。 needm
は、もし余分なMのリストが空になった場合、mp->needextram
フラグを設定します。これは、後でruntime·newextram
を呼び出して新しいMをリストに追加する必要があることを示します。
-
runtime·dropm
関数:- Cgoコールバックが完了し、GoコードからCコードに戻る際に呼び出されます。
dropm
は、needm
で借用したMを余分なMのリストに戻します。- これにより、Mは再利用可能になり、リソースのリークを防ぎます。
dropm
もまた、アトミック操作を使用してリストのロックとアンロックを行います。
-
runtime·newextram
関数:- 余分なMのリストが空になった場合に、新しいMを割り当ててリストに追加するために導入されました。
- この関数は、通常のGoランタイムのコンテキスト(MとGが適切に設定されている状態)で呼び出されるため、メモリ割り当てなどの通常のランタイム操作が可能です。
- 新しいMには、
runtime·goexit
をsched.pc
とするダミーのG(ゴルーチン)が割り当てられます。これは、トレースバック時にゴルーチンのスタックの終わりを示すためです。
-
runtime·setmg
関数:- 現在のスレッドのTLSにMとGを設定するためのアセンブリ関数です。
needm
やdropm
から呼び出されます。
- 現在のスレッドのTLSにMとGを設定するためのアセンブリ関数です。
-
runtime·allocm
の導入とruntime·newm
の変更:runtime·newm
は、OSスレッドを起動し、そのスレッドにMを関連付ける役割を担っていましたが、このコミットでruntime·allocm
が導入され、Mの割り当てと初期化のロジックが分離されました。allocm
はM構造体を割り当て、g0
(スケジューリングスタック)を初期化します。newm
はallocm
を呼び出してMを取得し、その後OS固有の関数(libcgo_thread_start
やruntime·newosproc
)を呼び出してOSスレッドを起動し、そのスレッドでruntime·mstart
を実行させます。
-
テストケースの追加:
misc/cgo/test/cthread.go
、misc/cgo/test/cthread_unix.c
、misc/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
の値を読み込みます。- もし
extram
がMLOCKED
であれば、他のスレッドがロックしているため、runtime·osyield()
を呼び出してCPUを譲り、再試行します。 nilokay
がfalse
でextram
がnil
の場合(リストが空の場合)、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->stackbase
とg->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.pc
をruntime·goexit
に設定します。これは、このGがCgoコールバックのコンテキストとして使用され、Goコードの実行が終了した際にgoexit
に到達することを示すためです。mp->curg = gp;
などでMとGを関連付けます。mnext = lockextra(true);
でextram
リストをロックし、mp->schedlink = mnext;
で新しいMをリストの先頭に挿入します。unlockextra(mp);
でロックを解除します。
- 新しいMを割り当てて
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);
でロックを解除します。
- Cgoコールバックが終了し、GoコードからCコードに戻る際に呼び出され、借用したMを
M* runtime·allocm(void)
:- M構造体を割り当て、その
g0
(スケジューリングスタック)を初期化します。 - この関数は、OSスレッドの起動とは独立してMを準備するために導入されました。
- M構造体を割り当て、その
src/pkg/runtime/asm_*.s
(アセンブリファイル)
各アーキテクチャのアセンブリファイル(asm_386.s
, asm_amd64.s
, asm_arm.s
)には、runtime·cgocallback
とruntime·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の相互運用性が大幅に向上しました。
関連リンク
- Go Issue #4435: https://github.com/golang/go/issues/4435
- Go CL 7304104: https://golang.org/cl/7304104
参考にした情報源リンク
- Go Issue #4435に関するWeb検索結果:
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGubRylFWT7N1vp5jmvKz1q9wS4REiMNRBYarvUbKR-RrlEUIMNmc_6ycvXooVmtc2CDZhRyVqHGfkE3-AIF0hKgJ-KiIdntb1O98hxe8iIcePQaX3GR9Vy_GrV1b5aI7sWvf3OttDgVxH7YuOC0us=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHQA-QyJ6RRLUr95F0gyU94eSlGSeu8R8Ys4c8P5lF59xWn9gzw4TL2YTx1oL9PpJelTHrOEjEacbMys5BmjEK2NIZyvZGvcOi9p1YCc4LM5jC2KUY4NLL65x9xE12ijAHumIjEW3dDafCx2JOUtPDrNyCuHctp9VVzsiTEvGqWrMhCMVZy
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEhZY9HCaatgbOWKqdR5wCHb0QAlHApoT9TWP1qrFzXo6WsAH2c0Q4JcGwf_pamztkAkaH0hP14Vrha4Q4iZp7oTle9hHeJiSrcEpwsrf7U-PGRihjxfPmOxqxhrDIClEJonFBrqwqh1c6iPTpTirhtDv2AaAtC-M19PfinY1MG