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

[インデックス 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スレッドで実行されるとは限らない状況が生じていました。

この変更では、以下の点が重要です。

  1. runtime.mainstart関数の廃止: 以前は、main.initmain.mainを呼び出すためのラッパーとしてruntime.mainstartという関数が存在し、これが新しいゴルーチンのエントリポイントとして使用されていました。このコミットでは、runtime.mainstartが削除され、代わりにruntime.mainという新しい関数が導入されます。
  2. runtime.main関数の導入とLockOSThreadの自動呼び出し: 新しく導入されたruntime.main関数は、プログラムの初期化中にメインゴルーチンのエントリポイントとなります。この関数内で、まずruntime.LockOSThread()が呼び出されます。これにより、main.init()main.main()が実行される間、メインゴルーチンがプログラムを起動したメインOSスレッドに固定されることが保証されます。
  3. 初期化中のLockOSThreadの特殊な振る舞い: proc.c内のruntime.LockOSThreadの実装が変更され、初期化フェーズ(runtime.sched.inittrueの場合)にruntime.LockOSThreadが呼び出された場合、実際にOSスレッドをロックするのではなく、runtime.sched.lockmainフラグをtrueに設定するだけになります。これは、m0(最初のOSスレッド)が初期化中に既にメインゴルーチンを実行しているため、明示的なロック操作は不要であり、フラグを設定するだけで十分であることを示唆しています。
  4. UnlockOSThreadの条件付き呼び出し: runtime.main関数内でmain.main()の実行後、runtime.sched.lockmainfalseの場合にのみ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

主要な変更点は以下の通りです。

  1. asm.s ファイル群 (386, amd64, arm):

    • runtime·mainstartというラベルを持つ関数が削除されました。
    • runtime·newprocを呼び出して新しいゴルーチンを作成する際に、エントリポイントとしてruntime·mainstart(SB)の代わりにruntime·main(SB)が渡されるようになりました。
  2. src/pkg/runtime/debug.go:

    • LockOSThread関数のコメントから「LockOSThread cannot be used during init functions.」という記述が削除されました。これは、このコミットによって初期化中にもLockOSThreadが安全に使用できるようになったことを示唆しています。
  3. src/pkg/runtime/proc.c:

    • Sched構造体にbool initbool lockmainという新しいフィールドが追加されました。
      • init: ランタイムが初期化中であるかどうかを示すフラグ。
      • lockmain: runtime.LockOSThreadが初期化中に呼び出され、メインゴルーチンをロックし続けることを意図しているかどうかを示すフラグ。
    • runtime·schedinit関数のコメントが更新され、新しいGがruntime·mainを呼び出すことが明記されました。
    • runtime·main関数が新しく追加されました。
      • この関数内で、まずruntime·LockOSThread()が呼び出されます。
      • runtime·sched.inittrueに設定され、main·init()が呼び出されます。
      • runtime·sched.initfalseに戻されます。
      • runtime·sched.lockmainfalseの場合にのみ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を設定するだけでリターンするようになりました。
  4. src/pkg/runtime/runtime.h:

    • runtime·LockOSThreadruntime·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
};

initlockmainという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;
}

これがこのコミットの最も重要な部分です。

  1. runtime·LockOSThread(): まず、この関数が呼び出されます。これにより、メインゴルーチンは現在のOSスレッド(プログラムを起動したメインOSスレッド)にロックされます。このロックは、main·init()main·main()の実行中、メインゴルーチンがこのスレッドから離れないことを保証します。
  2. runtime·sched.init = true;: ランタイムが初期化フェーズに入ったことを示すフラグを設定します。
  3. main·init();: mainパッケージのinit関数が呼び出されます。
  4. runtime·sched.init = false;: 初期化フェーズが終了したことを示すフラグをリセットします。
  5. if(!runtime·sched.lockmain) runtime·UnlockOSThread();: ここが重要です。もしruntime·sched.lockmainfalseであれば、runtime·UnlockOSThread()が呼び出され、メインゴルーチンはOSスレッドからアンロックされます。runtime·sched.lockmaintrueになるのは、ユーザーがmain·init内で明示的にruntime.LockOSThread()を呼び出した場合です。この条件により、ユーザーがメインゴルーチンをロックし続けることを意図した場合、ランタイムが勝手にアンロックしないようになります。
  6. main·main();: プログラムのメイン関数が呼び出されます。
  7. runtime·exit(0);: プログラムが正常終了します。

runtime·LockOSThreadruntime·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.inittrue)中に、かつ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関数に関する情報
  • 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

[インデックス 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スレッドで実行されるとは限らない状況が生じていました。

この変更では、以下の点が重要です。

  1. runtime.mainstart関数の廃止: 以前は、main.initmain.mainを呼び出すためのラッパーとしてruntime.mainstartという関数が存在し、これが新しいゴルーチンのエントリポイントとして使用されていました。このコミットでは、runtime.mainstartが削除され、代わりにruntime.mainという新しい関数が導入されます。
  2. runtime.main関数の導入とLockOSThreadの自動呼び出し: 新しく導入されたruntime.main関数は、プログラムの初期化中にメインゴルーチンのエントリポイントとなります。この関数内で、まずruntime.LockOSThread()が呼び出されます。これにより、main.init()main.main()が実行される間、メインゴルーチンがプログラムを起動したメインOSスレッドに固定されることが保証されます。
  3. 初期化中のLockOSThreadの特殊な振る舞い: proc.c内のruntime.LockOSThreadの実装が変更され、初期化フェーズ(runtime.sched.inittrueの場合)にruntime.LockOSThreadが呼び出された場合、実際にOSスレッドをロックするのではなく、runtime.sched.lockmainフラグをtrueに設定するだけになります。これは、m0(最初のOSスレッド)が初期化中に既にメインゴルーチンを実行しているため、明示的なロック操作は不要であり、フラグを設定するだけで十分であることを示唆しています。
  4. UnlockOSThreadの条件付き呼び出し: runtime.main関数内でmain.main()の実行後、runtime.sched.lockmainfalseの場合にのみ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

主要な変更点は以下の通りです。

  1. asm.s ファイル群 (386, amd64, arm):

    • runtime·mainstartというラベルを持つ関数が削除されました。
    • runtime·newprocを呼び出して新しいゴルーチンを作成する際に、エントリポイントとしてruntime·mainstart(SB)の代わりにruntime·main(SB)が渡されるようになりました。
  2. src/pkg/runtime/debug.go:

    • LockOSThread関数のコメントから「LockOSThread cannot be used during init functions.」という記述が削除されました。これは、このコミットによって初期化中にもLockOSThreadが安全に使用できるようになったことを示唆しています。
  3. src/pkg/runtime/proc.c:

    • Sched構造体にbool initbool lockmainという新しいフィールドが追加されました。
      • init: ランタイムが初期化中であるかどうかを示すフラグ。
      • lockmain: runtime.LockOSThreadが初期化中に呼び出され、メインゴルーチンをロックし続けることを意図しているかどうかを示すフラグ。
    • runtime·schedinit関数のコメントが更新され、新しいGがruntime·mainを呼び出すことが明記されました。
    • runtime·main関数が新しく追加されました。
      • この関数内で、まずruntime·LockOSThread()が呼び出されます。
      • runtime·sched.inittrueに設定され、main·init()が呼び出されます。
      • runtime·sched.initfalseに戻されます。
      • runtime·sched.lockmainfalseの場合にのみ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を設定するだけでリターンするようになりました。
  4. src/pkg/runtime/runtime.h:

    • runtime·LockOSThreadruntime·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
};

initlockmainという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;
}

これがこのコミットの最も重要な部分です。

  1. runtime·LockOSThread(): まず、この関数が呼び出されます。これにより、メインゴルーチンは現在のOSスレッド(プログラムを起動したメインOSスレッド)にロックされます。このロックは、main·init()main·main()の実行中、メインゴルーチンがこのスレッドから離れないことを保証します。
  2. runtime·sched.init = true;: ランタイムが初期化フェーズに入ったことを示すフラグを設定します。
  3. main·init();: mainパッケージのinit関数が呼び出されます。
  4. runtime·sched.init = false;: 初期化フェーズが終了したことを示すフラグをリセットします。
  5. if(!runtime·sched.lockmain) runtime·UnlockOSThread();: ここが重要です。もしruntime·sched.lockmainfalseであれば、runtime·UnlockOSThread()が呼び出され、メインゴルーチンはOSスレッドからアンロックされます。runtime·sched.lockmaintrueになるのは、ユーザーがmain·init内で明示的にruntime.LockOSThread()を呼び出した場合です。この条件により、ユーザーがメインゴルーチンをロックし続けることを意図した場合、ランタイムが勝手にアンロックしないようになります。
  6. main·main();: プログラムのメイン関数が呼び出されます。
  7. runtime·exit(0);: プログラムが正常終了します。

runtime·LockOSThreadruntime·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.inittrue)中に、かつm0(プログラムを起動した最初のOSスレッド)上で呼び出された場合に特殊な振る舞いをするようになりました。

  • LockOSThreadの場合、実際にゴルーチンをOSスレッドにロックする代わりに、runtime·sched.lockmainフラグをtrueに設定してすぐにリターンします。これは、m0が既にメインゴルーチンを実行しているため、物理的なロック操作は不要であり、意図をフラグで示すだけで十分だからです。
  • UnlockOSThreadの場合も同様に、runtime·sched.lockmainフラグをfalseに設定してすぐにリターンします。

これらの変更により、Goプログラムの初期化プロセスがより堅牢になり、特定のOSスレッドでの実行を必要とする既存のコードベースとの互換性が向上しました。

関連リンク

  • Go Change List: https://golang.org/cl/5309070

参考にした情報源リンク