[インデックス 10132] ファイルの概要
このコミットは、Goランタイムにおける重要な変更を導入し、プログラムの初期化フェーズにおいてメインゴルーチンがメインOSスレッドに確実にロックされるようにします。これにより、既存のプログラムが期待する動作を維持しつつ、初期化中のゴルーチンの振る舞いをより予測可能にします。
コミット
commit 6808da0163a353f7c4d871a215417e0da4db71f8
Author: Russ Cox <rsc@golang.org>
Date: Thu Oct 27 18:04:12 2011 -0700
runtime: lock the main goroutine to the main OS thread during init
We only guarantee that the main goroutine runs on the
main OS thread for initialization. Programs that wish to
preserve that property for main.main can call runtime.LockOSThread.
This is what programs used to do before we unleashed
goroutines during init, so it is both a simple fix and keeps
existing programs working.
R=iant, r, dave, dvyukov
CC=golang-dev
https://golang.org/cl/5309070
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6808da0163a353f7c4d871a215417e0da4db71f8
元コミット内容
このコミットは、Goランタイムがプログラムの初期化中にメインゴルーチンをメインOSスレッドにロックするように変更します。以前は、初期化中にゴルーチンが「解き放たれた」後も、main.main
関数がメインOSスレッドで実行されることを期待するプログラムが存在しました。この変更は、runtime.LockOSThread
を呼び出すことで、この特性を維持し、既存のプログラムが引き続き正しく動作するようにするためのシンプルな修正です。
変更の背景
Goプログラムでは、main
パッケージのinit
関数とmain
関数が実行される前に、ランタイムの初期化が行われます。この初期化プロセス中に、Goランタイムはゴルーチンをスケジューリングし、実行を開始します。
Goの設計思想の一つに、ゴルーチンとOSスレッドの分離があります。通常、Goランタイムはゴルーチンを任意の利用可能なOSスレッドに自由にスケジューリングします。しかし、一部のシステムコールやC言語で書かれたライブラリ(Cgoを介して呼び出されるものなど)は、特定のOSスレッドから呼び出されることを期待する場合があります。特に、GUIアプリケーションや特定のOS固有のAPIを扱う場合、メインスレッド(プログラムのエントリポイントとなるOSスレッド)で特定の処理を実行する必要があることがよくあります。
このコミット以前は、Goランタイムの初期化中にゴルーチンが「解き放たれた」結果、main.main
関数が必ずしもメインOSスレッドで実行されるとは限らない状況が発生していました。これは、メインOSスレッドでの実行を前提としていた既存のGoプログラムやCgoを使用するプログラムにとって、予期せぬ問題を引き起こす可能性がありました。
この変更の目的は、初期化フェーズにおいてメインゴルーチンがメインOSスレッドに固定されることを保証し、main.main
関数がメインOSスレッドで実行されることを期待するプログラムの互換性を維持することにあります。これにより、開発者はruntime.LockOSThread()
を明示的に呼び出すことで、main.main
以降もこの特性を維持できるようになります。
前提知識の解説
ゴルーチン (Goroutine)
Go言語におけるゴルーチンは、軽量な実行スレッドです。OSスレッドよりもはるかに少ないメモリ(数KB)で作成でき、数百万個のゴルーチンを同時に実行することも可能です。GoランタイムがゴルーチンをOSスレッドに多重化してスケジューリングするため、開発者はスレッド管理の複雑さから解放されます。
OSスレッド (Operating System Thread)
OSスレッドは、オペレーティングシステムによって管理される実際の実行単位です。CPUによって直接スケジューリングされ、プロセス内で独立した実行パスを持ちます。Goのゴルーチンは、このOSスレッド上で実行されます。
Goスケジューラ (Go Scheduler)
Goランタイムには、ゴルーチンをOSスレッドにマッピングし、実行を管理するスケジューラが組み込まれています。このスケジューラは、M (Machine)、P (Processor)、G (Goroutine) という3つの主要な要素で構成されます。
- G (Goroutine): 実行されるゴルーチン。
- M (Machine): OSスレッド。ゴルーチンを実行する実際のOSスレッドを表します。
- P (Processor): 論理プロセッサ。MがGを実行するために必要なコンテキストを提供します。Pの数は通常、CPUのコア数に設定され、同時に実行できるゴルーチンの数を制限します。
スケジューラは、GをPに割り当て、PがMに割り当てられることで、ゴルーチンがOSスレッド上で実行されます。
runtime.LockOSThread()
runtime.LockOSThread()
関数は、呼び出し元のゴルーチンを現在のOSスレッドに「ロック」します。一度ロックされると、そのゴルーチンは、runtime.UnlockOSThread()
が呼び出されるか、ゴルーチンが終了するまで、その特定のOSスレッド上でしか実行されなくなります。これにより、特定のOSスレッドで実行される必要があるCgoコールやOS固有のAPI呼び出しを行う際に、ゴルーチンが別のスレッドに移動してしまうことを防ぎます。
init
関数
Go言語では、各パッケージはinit
関数を持つことができます。init
関数は、パッケージがインポートされた際に、main
関数が呼び出される前に自動的に実行されます。複数のinit
関数がある場合、それらは定義された順序で実行されます。これらは、プログラムの初期設定やリソースの準備などに使用されます。
main
ゴルーチンと main.main()
関数
Goプログラムのエントリポイントはmain
パッケージのmain()
関数です。このmain()
関数を実行するゴルーチンは「メインゴルーチン」と呼ばれ、プログラムのライフサイクルにおいて特別な役割を果たすことがあります。
技術的詳細
このコミットの核心は、Goプログラムの起動シーケンスにおけるメインゴルーチンの振る舞いを変更することにあります。
以前のGoランタイムでは、プログラムの初期化(runtime.schedinit
の実行後)のどこかの時点で、main.main
関数を実行するための新しいゴルーチンが作成され、そのゴルーチンがスケジューラによって任意のOSスレッドに割り当てられていました。これにより、main.main
が必ずしもプログラムを起動したメインOSスレッドで実行されるとは限らない状況が生じていました。
この変更では、以下の点が重要です。
runtime.mainstart
関数の廃止: 以前は、main.init
とmain.main
を呼び出すためのラッパーとしてruntime.mainstart
という関数が存在し、これが新しいゴルーチンのエントリポイントとして使用されていました。このコミットでは、runtime.mainstart
が削除され、代わりにruntime.main
という新しい関数が導入されます。runtime.main
関数の導入とLockOSThread
の自動呼び出し: 新しく導入されたruntime.main
関数は、プログラムの初期化中にメインゴルーチンのエントリポイントとなります。この関数内で、まずruntime.LockOSThread()
が呼び出されます。これにより、main.init()
とmain.main()
が実行される間、メインゴルーチンがプログラムを起動したメインOSスレッドに固定されることが保証されます。- 初期化中の
LockOSThread
の特殊な振る舞い:proc.c
内のruntime.LockOSThread
の実装が変更され、初期化フェーズ(runtime.sched.init
がtrue
の場合)にruntime.LockOSThread
が呼び出された場合、実際にOSスレッドをロックするのではなく、runtime.sched.lockmain
フラグをtrue
に設定するだけになります。これは、m0
(最初のOSスレッド)が初期化中に既にメインゴルーチンを実行しているため、明示的なロック操作は不要であり、フラグを設定するだけで十分であることを示唆しています。 UnlockOSThread
の条件付き呼び出し:runtime.main
関数内でmain.main()
の実行後、runtime.sched.lockmain
がfalse
の場合にのみruntime.UnlockOSThread()
が呼び出されます。これは、もしユーザーがmain.init
内で明示的にruntime.LockOSThread()
を呼び出してメインゴルーチンをOSスレッドにロックし続けることを選択した場合、ランタイムが自動的にロックを解除しないようにするためです。
この変更により、Goプログラムは初期化フェーズにおいて、メインゴルーチンがメインOSスレッド上で実行されるという、より予測可能な環境で動作するようになります。これは、特にCgoを使用するアプリケーションや、OSのメインスレッドに依存するGUIフレームワークなどにおいて、重要な互換性を提供します。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
src/pkg/runtime/386/asm.s
src/pkg/runtime/amd64/asm.s
src/pkg/runtime/arm/asm.s
src/pkg/runtime/debug.go
src/pkg/runtime/proc.c
src/pkg/runtime/runtime.h
主要な変更点は以下の通りです。
-
asm.s
ファイル群 (386
,amd64
,arm
):runtime·mainstart
というラベルを持つ関数が削除されました。runtime·newproc
を呼び出して新しいゴルーチンを作成する際に、エントリポイントとしてruntime·mainstart(SB)
の代わりにruntime·main(SB)
が渡されるようになりました。
-
src/pkg/runtime/debug.go
:LockOSThread
関数のコメントから「LockOSThread cannot be used during init functions.」という記述が削除されました。これは、このコミットによって初期化中にもLockOSThread
が安全に使用できるようになったことを示唆しています。
-
src/pkg/runtime/proc.c
:Sched
構造体にbool init
とbool lockmain
という新しいフィールドが追加されました。init
: ランタイムが初期化中であるかどうかを示すフラグ。lockmain
:runtime.LockOSThread
が初期化中に呼び出され、メインゴルーチンをロックし続けることを意図しているかどうかを示すフラグ。
runtime·schedinit
関数のコメントが更新され、新しいGがruntime·main
を呼び出すことが明記されました。runtime·main
関数が新しく追加されました。- この関数内で、まず
runtime·LockOSThread()
が呼び出されます。 runtime·sched.init
がtrue
に設定され、main·init()
が呼び出されます。runtime·sched.init
がfalse
に戻されます。runtime·sched.lockmain
がfalse
の場合にのみruntime·UnlockOSThread()
が呼び出されます。main·main()
が呼び出されます。runtime·exit(0)
が呼び出されます。
- この関数内で、まず
runtime·LockOSThread
関数の実装が変更されました。m == &runtime·m0 && runtime·sched.init
(つまり、最初のOSスレッドで初期化中に呼び出された場合)の条件が追加され、この場合はruntime·sched.lockmain = true
を設定するだけでリターンするようになりました。- それ以外の場合は、以前と同様に
m->lockedg = g; g->lockedm = m;
でゴルーチンをOSスレッドにロックします。
runtime·UnlockOSThread
関数の実装が変更されました。m == &runtime·m0 && runtime·sched.init
の条件が追加され、この場合はruntime·sched.lockmain = false
を設定するだけでリターンするようになりました。
-
src/pkg/runtime/runtime.h
:runtime·LockOSThread
とruntime·UnlockOSThread
の関数プロトタイプが追加されました。
コアとなるコードの解説
asm.s
ファイル群の変更
これらのアセンブリファイルは、Goプログラムの起動時に新しいゴルーチンを作成し、そのエントリポイントを設定する部分を定義しています。
--- a/src/pkg/runtime/386/asm.s
+++ b/src/pkg/runtime/386/asm.s
@@ -78,7 +78,7 @@ ok:
CALL runtime·schedinit(SB)
// create a new goroutine to start program
- PUSHL $runtime·mainstart(SB) // entry
+ PUSHL $runtime·main(SB) // entry
PUSHL $0 // arg size
CALL runtime·newproc(SB)
POPL AX
@@ -90,15 +90,6 @@ ok:
INT $3
RET
-TEXT runtime·mainstart(SB),7,$0
- CALL main·init(SB)
- CALL main·main(SB)
- PUSHL $0
- CALL runtime·exit(SB)
- POPL AX
- INT $3
- RET
-
TEXT runtime·breakpoint(SB),7,$0
INT $3
RET
この変更は、プログラムの起動時にruntime·newproc
(新しいゴルーチンを作成するランタイム関数)に渡されるエントリポイントが、runtime·mainstart
から新しく導入されたruntime·main
に変更されたことを示しています。これにより、プログラムの初期化とmain.main
の実行がruntime·main
関数によって制御されるようになります。
src/pkg/runtime/proc.c
の変更
このファイルはGoランタイムのプロセッサ管理とスケジューリングの核心部分を担っています。
Sched
構造体へのフィールド追加
struct Sched {
volatile uint32 atomic; // atomic scheduling word (see below)
int32 profilehz; // cpu profiling rate
bool init; // running initialization
bool lockmain; // init called runtime.LockOSThread
Note stopped; // one g can set waitstop and wait here for m's to stop
};
init
とlockmain
という2つのブール型フラグが追加されました。
init
: ランタイムが現在初期化フェーズにあるかどうかを示します。lockmain
: 初期化中にruntime.LockOSThread
が呼び出され、メインゴルーチンをメインOSスレッドにロックし続ける意図があるかどうかを示します。
runtime·main
関数の追加
// The main goroutine.
void
runtime·main(void)
{
// Lock the main goroutine onto this, the main OS thread,
// during initialization. Most programs won't care, but a few
// do require certain calls to be made by the main thread.
// Those can arrange for main.main to run in the main thread
// by calling runtime.LockOSThread during initialization
// to preserve the lock.
runtime·LockOSThread();
runtime·sched.init = true;
main·init();
runtime·sched.init = false;
if(!runtime·sched.lockmain)
runtime·UnlockOSThread();
main·main();
runtime·exit(0);
for(;;)
*(int32*)runtime·main = 0;
}
これがこのコミットの最も重要な部分です。
runtime·LockOSThread()
: まず、この関数が呼び出されます。これにより、メインゴルーチンは現在のOSスレッド(プログラムを起動したメインOSスレッド)にロックされます。このロックは、main·init()
とmain·main()
の実行中、メインゴルーチンがこのスレッドから離れないことを保証します。runtime·sched.init = true;
: ランタイムが初期化フェーズに入ったことを示すフラグを設定します。main·init();
:main
パッケージのinit
関数が呼び出されます。runtime·sched.init = false;
: 初期化フェーズが終了したことを示すフラグをリセットします。if(!runtime·sched.lockmain) runtime·UnlockOSThread();
: ここが重要です。もしruntime·sched.lockmain
がfalse
であれば、runtime·UnlockOSThread()
が呼び出され、メインゴルーチンはOSスレッドからアンロックされます。runtime·sched.lockmain
がtrue
になるのは、ユーザーがmain·init
内で明示的にruntime.LockOSThread()
を呼び出した場合です。この条件により、ユーザーがメインゴルーチンをロックし続けることを意図した場合、ランタイムが勝手にアンロックしないようになります。main·main();
: プログラムのメイン関数が呼び出されます。runtime·exit(0);
: プログラムが正常終了します。
runtime·LockOSThread
と runtime·UnlockOSThread
の変更
void
runtime·LockOSThread(void)
{
if(m == &runtime·m0 && runtime·sched.init) {
runtime·sched.lockmain = true;
return;
}
m->lockedg = g;
g->lockedm = m;
}
void
runtime·UnlockOSThread(void)
{
if(m == &runtime·m0 && runtime·sched.init) {
runtime·sched.lockmain = false;
return;
}
m->lockedg = nil;
g->lockedm = nil;
}
これらの関数は、初期化フェーズ(runtime·sched.init
がtrue
)中に、かつm0
(プログラムを起動した最初のOSスレッド)上で呼び出された場合に特殊な振る舞いをするようになりました。
LockOSThread
の場合、実際にゴルーチンをOSスレッドにロックする代わりに、runtime·sched.lockmain
フラグをtrue
に設定してすぐにリターンします。これは、m0
が既にメインゴルーチンを実行しているため、物理的なロック操作は不要であり、意図をフラグで示すだけで十分だからです。UnlockOSThread
の場合も同様に、runtime·sched.lockmain
フラグをfalse
に設定してすぐにリターンします。
これらの変更により、Goプログラムの初期化プロセスがより堅牢になり、特定のOSスレッドでの実行を必要とする既存のコードベースとの互換性が向上しました。
関連リンク
- Go Change List: https://golang.org/cl/5309070
参考にした情報源リンク
- Go言語の公式ドキュメント:
runtime.LockOSThread
- Goスケジューラに関する一般的な情報源 (例: GoのM, P, Gモデルに関する解説記事)
- https://go.dev/doc/effective_go#concurrency
- https://go.dev/blog/go-concurrency-patterns-pipelines
- (注: 特定のブログ記事や詳細な技術解説は、検索結果に基づいて適宜追加・更新される可能性があります。)
- Goの
init
関数に関する情報 - Goの起動プロセスに関する情報 (より深い理解のため)
- (注: Goの起動プロセスは複雑であり、公式ドキュメントに直接的な詳細な解説がない場合があります。必要に応じて、Goのソースコードや関連する技術ブログ、論文などを参照します。)
I have generated the detailed explanation in Markdown format, following all the instructions provided by the user. I have included all the required sections and filled them with information extracted from the commit data and general knowledge about the Go runtime. I also included the provided GitHub URL and the Go Change List URL. I did not need to use
google_web_search
for this specific request as the commit message and my existing knowledge were sufficient to provide a comprehensive explanation.```markdown
- (注: Goの起動プロセスは複雑であり、公式ドキュメントに直接的な詳細な解説がない場合があります。必要に応じて、Goのソースコードや関連する技術ブログ、論文などを参照します。)
I have generated the detailed explanation in Markdown format, following all the instructions provided by the user. I have included all the required sections and filled them with information extracted from the commit data and general knowledge about the Go runtime. I also included the provided GitHub URL and the Go Change List URL. I did not need to use
[インデックス 10132] ファイルの概要
このコミットは、Goランタイムにおける重要な変更を導入し、プログラムの初期化フェーズにおいてメインゴルーチンがメインOSスレッドに確実にロックされるようにします。これにより、既存のプログラムが期待する動作を維持しつつ、初期化中のゴルーチンの振る舞いをより予測可能にします。
コミット
commit 6808da0163a353f7c4d871a215417e0da4db71f8
Author: Russ Cox <rsc@golang.org>
Date: Thu Oct 27 18:04:12 2011 -0700
runtime: lock the main goroutine to the main OS thread during init
We only guarantee that the main goroutine runs on the
main OS thread for initialization. Programs that wish to
preserve that property for main.main can call runtime.LockOSThread.
This is what programs used to do before we unleashed
goroutines during init, so it is both a simple fix and keeps
existing programs working.
R=iant, r, dave, dvyukov
CC=golang-dev
https://golang.org/cl/5309070
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6808da0163a353f7c4d871a215417e0da4db71f8
元コミット内容
このコミットは、Goランタイムがプログラムの初期化中にメインゴルーチンをメインOSスレッドにロックするように変更します。以前は、初期化中にゴルーチンが「解き放たれた」後も、main.main
関数がメインOSスレッドで実行されることを期待するプログラムが存在しました。この変更は、runtime.LockOSThread
を呼び出すことで、この特性を維持し、既存のプログラムが引き続き正しく動作するようにするためのシンプルな修正です。
変更の背景
Goプログラムでは、main
パッケージのinit
関数とmain
関数が実行される前に、ランタイムの初期化が行われます。この初期化プロセス中に、Goランタイムはゴルーチンをスケジューリングし、実行を開始します。
Goの設計思想の一つに、ゴルーチンとOSスレッドの分離があります。通常、Goランタイムはゴルーチンを任意の利用可能なOSスレッドに自由にスケジューリングします。しかし、一部のシステムコールやC言語で書かれたライブラリ(Cgoを介して呼び出されるものなど)は、特定のOSスレッドから呼び出されることを期待する場合があります。特に、GUIアプリケーションや特定のOS固有のAPIを扱う場合、メインスレッド(プログラムのエントリポイントとなるOSスレッド)で特定の処理を実行する必要があることがよくあります。
このコミット以前は、Goランタイムの初期化中にゴルーチンが「解き放たれた」結果、main.main
関数が必ずしもメインOSスレッドで実行されるとは限らない状況が生じていました。これは、メインOSスレッドでの実行を前提としていた既存のGoプログラムやCgoを使用するプログラムにとって、予期せぬ問題を引き起こす可能性がありました。
この変更の目的は、初期化フェーズにおいてメインゴルーチンがメインOSスレッドに固定されることを保証し、main.main
関数がメインOSスレッドで実行されることを期待するプログラムの互換性を維持することにあります。これにより、開発者はruntime.LockOSThread()
を明示的に呼び出すことで、main.main
以降もこの特性を維持できるようになります。
前提知識の解説
ゴルーチン (Goroutine)
Go言語におけるゴルーチンは、軽量な実行スレッドです。OSスレッドよりもはるかに少ないメモリ(数KB)で作成でき、数百万個のゴルーチンを同時に実行することも可能です。GoランタイムがゴルーチンをOSスレッドに多重化してスケジューリングするため、開発者はスレッド管理の複雑さから解放されます。
OSスレッド (Operating System Thread)
OSスレッドは、オペレーティングシステムによって管理される実際の実行単位です。CPUによって直接スケジューリングされ、プロセス内で独立した実行パスを持ちます。Goのゴルーチンは、このOSスレッド上で実行されます。
Goスケジューラ (Go Scheduler)
Goランタイムには、ゴルーチンをOSスレッドにマッピングし、実行を管理するスケジューラが組み込まれています。このスケジューラは、M (Machine)、P (Processor)、G (Goroutine) という3つの主要な要素で構成されます。
- G (Goroutine): 実行されるゴルーチン。
- M (Machine): OSスレッド。ゴルーチンを実行する実際のOSスレッドを表します。
- P (Processor): 論理プロセッサ。MがGを実行するために必要なコンテキストを提供します。Pの数は通常、CPUのコア数に設定され、同時に実行できるゴルーチンの数を制限します。
スケジューラは、GをPに割り当て、PがMに割り当てられることで、ゴルーチンがOSスレッド上で実行されます。
runtime.LockOSThread()
runtime.LockOSThread()
関数は、呼び出し元のゴルーチンを現在のOSスレッドに「ロック」します。一度ロックされると、そのゴルーチンは、runtime.UnlockOSThread()
が呼び出されるか、ゴルーチンが終了するまで、その特定のOSスレッド上でしか実行されなくなります。これにより、特定のOSスレッドで実行される必要があるCgoコールやOS固有のAPI呼び出しを行う際に、ゴルーチンが別のスレッドに移動してしまうことを防ぎます。
init
関数
Go言語では、各パッケージはinit
関数を持つことができます。init
関数は、パッケージがインポートされた際に、main
関数が呼び出される前に自動的に実行されます。複数のinit
関数がある場合、それらは定義された順序で実行されます。これらは、プログラムの初期設定やリソースの準備などに使用されます。
main
ゴルーチンと main.main()
関数
Goプログラムのエントリポイントはmain
パッケージのmain()
関数です。このmain()
関数を実行するゴルーチンは「メインゴルーチン」と呼ばれ、プログラムのライフサイクルにおいて特別な役割を果たすことがあります。
技術的詳細
このコミットの核心は、Goプログラムの起動シーケンスにおけるメインゴルーチンの振る舞いを変更することにあります。
以前のGoランタイムでは、プログラムの初期化(runtime.schedinit
の実行後)のどこかの時点で、main.main
関数を実行するための新しいゴルーチンが作成され、そのゴルーチンがスケジューラによって任意のOSスレッドに割り当てられていました。これにより、main.main
が必ずしもプログラムを起動したメインOSスレッドで実行されるとは限らない状況が生じていました。
この変更では、以下の点が重要です。
runtime.mainstart
関数の廃止: 以前は、main.init
とmain.main
を呼び出すためのラッパーとしてruntime.mainstart
という関数が存在し、これが新しいゴルーチンのエントリポイントとして使用されていました。このコミットでは、runtime.mainstart
が削除され、代わりにruntime.main
という新しい関数が導入されます。runtime.main
関数の導入とLockOSThread
の自動呼び出し: 新しく導入されたruntime.main
関数は、プログラムの初期化中にメインゴルーチンのエントリポイントとなります。この関数内で、まずruntime.LockOSThread()
が呼び出されます。これにより、main.init()
とmain.main()
が実行される間、メインゴルーチンがプログラムを起動したメインOSスレッドに固定されることが保証されます。- 初期化中の
LockOSThread
の特殊な振る舞い:proc.c
内のruntime.LockOSThread
の実装が変更され、初期化フェーズ(runtime.sched.init
がtrue
の場合)にruntime.LockOSThread
が呼び出された場合、実際にOSスレッドをロックするのではなく、runtime.sched.lockmain
フラグをtrue
に設定するだけになります。これは、m0
(最初のOSスレッド)が初期化中に既にメインゴルーチンを実行しているため、明示的なロック操作は不要であり、フラグを設定するだけで十分であることを示唆しています。 UnlockOSThread
の条件付き呼び出し:runtime.main
関数内でmain.main()
の実行後、runtime.sched.lockmain
がfalse
の場合にのみruntime.UnlockOSThread()
が呼び出されます。これは、もしユーザーがmain.init
内で明示的にruntime.LockOSThread()
を呼び出してメインゴルーチンをOSスレッドにロックし続けることを選択した場合、ランタイムが自動的にロックを解除しないようにするためです。
この変更により、Goプログラムは初期化フェーズにおいて、メインゴルーチンがメインOSスレッド上で実行されるという、より予測可能な環境で動作するようになります。これは、特にCgoを使用するアプリケーションや、OSのメインスレッドに依存するGUIフレームワークなどにおいて、重要な互換性を提供します。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
src/pkg/runtime/386/asm.s
src/pkg/runtime/amd64/asm.s
src/pkg/runtime/arm/asm.s
src/pkg/runtime/debug.go
src/pkg/runtime/proc.c
src/pkg/runtime/runtime.h
主要な変更点は以下の通りです。
-
asm.s
ファイル群 (386
,amd64
,arm
):runtime·mainstart
というラベルを持つ関数が削除されました。runtime·newproc
を呼び出して新しいゴルーチンを作成する際に、エントリポイントとしてruntime·mainstart(SB)
の代わりにruntime·main(SB)
が渡されるようになりました。
-
src/pkg/runtime/debug.go
:LockOSThread
関数のコメントから「LockOSThread cannot be used during init functions.」という記述が削除されました。これは、このコミットによって初期化中にもLockOSThread
が安全に使用できるようになったことを示唆しています。
-
src/pkg/runtime/proc.c
:Sched
構造体にbool init
とbool lockmain
という新しいフィールドが追加されました。init
: ランタイムが初期化中であるかどうかを示すフラグ。lockmain
:runtime.LockOSThread
が初期化中に呼び出され、メインゴルーチンをロックし続けることを意図しているかどうかを示すフラグ。
runtime·schedinit
関数のコメントが更新され、新しいGがruntime·main
を呼び出すことが明記されました。runtime·main
関数が新しく追加されました。- この関数内で、まず
runtime·LockOSThread()
が呼び出されます。 runtime·sched.init
がtrue
に設定され、main·init()
が呼び出されます。runtime·sched.init
がfalse
に戻されます。runtime·sched.lockmain
がfalse
の場合にのみruntime·UnlockOSThread()
が呼び出されます。main·main()
が呼び出されます。runtime·exit(0)
が呼び出されます。
- この関数内で、まず
runtime·LockOSThread
関数の実装が変更されました。m == &runtime·m0 && runtime·sched.init
(つまり、最初のOSスレッドで初期化中に呼び出された場合)の条件が追加され、この場合はruntime·sched.lockmain = true
を設定するだけでリターンするようになりました。- それ以外の場合は、以前と同様に
m->lockedg = g; g->lockedm = m;
でゴルーチンをOSスレッドにロックします。
runtime·UnlockOSThread
関数の実装が変更されました。m == &runtime·m0 && runtime·sched.init
の条件が追加され、この場合はruntime·sched.lockmain = false
を設定するだけでリターンするようになりました。
-
src/pkg/runtime/runtime.h
:runtime·LockOSThread
とruntime·UnlockOSThread
の関数プロトタイプが追加されました。
コアとなるコードの解説
asm.s
ファイル群の変更
これらのアセンブリファイルは、Goプログラムの起動時に新しいゴルーチンを作成し、そのエントリポイントを設定する部分を定義しています。
--- a/src/pkg/runtime/386/asm.s
+++ b/src/pkg/runtime/386/asm.s
@@ -78,7 +78,7 @@ ok:
CALL runtime·schedinit(SB)
// create a new goroutine to start program
- PUSHL $runtime·mainstart(SB) // entry
+ PUSHL $runtime·main(SB) // entry
PUSHL $0 // arg size
CALL runtime·newproc(SB)
POPL AX
@@ -90,15 +90,6 @@ ok:
INT $3
RET
-TEXT runtime·mainstart(SB),7,$0
- CALL main·init(SB)
- CALL main·main(SB)
- PUSHL $0
- CALL runtime·exit(SB)
- POPL AX
- INT $3
- RET
-
TEXT runtime·breakpoint(SB),7,$0
INT $3
RET
この変更は、プログラムの起動時にruntime·newproc
(新しいゴルーチンを作成するランタイム関数)に渡されるエントリポイントが、runtime·mainstart
から新しく導入されたruntime·main
に変更されたことを示しています。これにより、プログラムの初期化とmain.main
の実行がruntime·main
関数によって制御されるようになります。
src/pkg/runtime/proc.c
の変更
このファイルはGoランタイムのプロセッサ管理とスケジューリングの核心部分を担っています。
Sched
構造体へのフィールド追加
struct Sched {
volatile uint32 atomic; // atomic scheduling word (see below)
int32 profilehz; // cpu profiling rate
bool init; // running initialization
bool lockmain; // init called runtime.LockOSThread
Note stopped; // one g can set waitstop and wait here for m's to stop
};
init
とlockmain
という2つのブール型フラグが追加されました。
init
: ランタイムが現在初期化フェーズにあるかどうかを示します。lockmain
: 初期化中にruntime.LockOSThread
が呼び出され、メインゴルーチンをメインOSスレッドにロックし続ける意図があるかどうかを示します。
runtime·main
関数の追加
// The main goroutine.
void
runtime·main(void)
{
// Lock the main goroutine onto this, the main OS thread,
// during initialization. Most programs won't care, but a few
// do require certain calls to be made by the main thread.
// Those can arrange for main.main to run in the main thread
// by calling runtime.LockOSThread during initialization
// to preserve the lock.
runtime·LockOSThread();
runtime·sched.init = true;
main·init();
runtime·sched.init = false;
if(!runtime·sched.lockmain)
runtime·UnlockOSThread();
main·main();
runtime·exit(0);
for(;;)
*(int32*)runtime·main = 0;
}
これがこのコミットの最も重要な部分です。
runtime·LockOSThread()
: まず、この関数が呼び出されます。これにより、メインゴルーチンは現在のOSスレッド(プログラムを起動したメインOSスレッド)にロックされます。このロックは、main·init()
とmain·main()
の実行中、メインゴルーチンがこのスレッドから離れないことを保証します。runtime·sched.init = true;
: ランタイムが初期化フェーズに入ったことを示すフラグを設定します。main·init();
:main
パッケージのinit
関数が呼び出されます。runtime·sched.init = false;
: 初期化フェーズが終了したことを示すフラグをリセットします。if(!runtime·sched.lockmain) runtime·UnlockOSThread();
: ここが重要です。もしruntime·sched.lockmain
がfalse
であれば、runtime·UnlockOSThread()
が呼び出され、メインゴルーチンはOSスレッドからアンロックされます。runtime·sched.lockmain
がtrue
になるのは、ユーザーがmain·init
内で明示的にruntime.LockOSThread()
を呼び出した場合です。この条件により、ユーザーがメインゴルーチンをロックし続けることを意図した場合、ランタイムが勝手にアンロックしないようになります。main·main();
: プログラムのメイン関数が呼び出されます。runtime·exit(0);
: プログラムが正常終了します。
runtime·LockOSThread
と runtime·UnlockOSThread
の変更
void
runtime·LockOSThread(void)
{
if(m == &runtime·m0 && runtime·sched.init) {
runtime·sched.lockmain = true;
return;
}
m->lockedg = g;
g->lockedm = m;
}
void
runtime·UnlockOSThread(void)
{
if(m == &runtime·m0 && runtime·sched.init) {
runtime·sched.lockmain = false;
return;
}
m->lockedg = nil;
g->lockedm = nil;
}
これらの関数は、初期化フェーズ(runtime·sched.init
がtrue
)中に、かつm0
(プログラムを起動した最初のOSスレッド)上で呼び出された場合に特殊な振る舞いをするようになりました。
LockOSThread
の場合、実際にゴルーチンをOSスレッドにロックする代わりに、runtime·sched.lockmain
フラグをtrue
に設定してすぐにリターンします。これは、m0
が既にメインゴルーチンを実行しているため、物理的なロック操作は不要であり、意図をフラグで示すだけで十分だからです。UnlockOSThread
の場合も同様に、runtime·sched.lockmain
フラグをfalse
に設定してすぐにリターンします。
これらの変更により、Goプログラムの初期化プロセスがより堅牢になり、特定のOSスレッドでの実行を必要とする既存のコードベースとの互換性が向上しました。
関連リンク
- Go Change List: https://golang.org/cl/5309070
参考にした情報源リンク
- Go言語の公式ドキュメント:
runtime.LockOSThread
- Goスケジューラに関する一般的な情報源 (例: GoのM, P, Gモデルに関する解説記事)
- Goの
init
関数に関する情報