[インデックス 12306] ファイルの概要
このコミットは、Goランタイムの初期化処理、特にinit
関数の実行がメインスレッド上で行われるように変更するものです。これにより、特定のOSスレッドに依存する処理(例えばGUIライブラリの初期化など)がGoプログラムの起動時に正しく動作することを保証します。また、デッドロック検出ロジックも調整されています。
コミット
- コミットハッシュ:
dc159fabff52e9dd3da0948438017373be741b22
- 作者: Russ Cox rsc@golang.org
- 日付: 2012年3月1日 木曜日 11:48:17 -0500
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/dc159fabff52e9dd3da0948438017373be741b22
元コミット内容
runtime: run init on main thread
Fixes #3125.
R=golang-dev, r, minux.ma
CC=golang-dev
https://golang.org/cl/5714049
変更の背景
Goプログラムの起動時、パッケージの初期化コード(init
関数)はGoランタイムによって自動的に実行されます。しかし、特定のシステムコールやGUIライブラリなど、一部の処理はプログラムが起動したOSのメインスレッド(通常はプロセスを起動したスレッド)で実行されることを期待します。
このコミット以前は、init
関数が必ずしもメインスレッドで実行される保証がありませんでした。これにより、init
関数内でruntime.LockOSThread()
を呼び出して現在のゴルーチンをOSスレッドにロックしようとした場合でも、それがメインスレッドではない別のスレッドにロックされてしまう可能性がありました。結果として、メインスレッドでの実行を前提とする外部ライブラリとの連携において問題が発生する可能性がありました。
この変更は、init
関数が確実にメインスレッド上で実行されるようにすることで、このような問題を解決し、Goプログラムがより広範なシステム環境や外部ライブラリと互換性を持つようにすることを目的としています。また、関連するデッドロック検出ロジックも、この変更に合わせて調整されています。
前提知識の解説
Goランタイム (Go Runtime)
Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクタ、スケジューラ(ゴルーチンをOSスレッドにマッピングする)、メモリ管理、システムコールインターフェースなどが含まれます。Goプログラムは、OSによって直接実行されるのではなく、このランタイム上で動作します。
ゴルーチン (Goroutine)
ゴルーチンはGo言語における軽量な並行処理の単位です。OSスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行できます。Goランタイムのスケジューラが、これらのゴルーチンを限られた数のOSスレッドに効率的にマッピングして実行します。
OSスレッド (OS Thread)
OSスレッドは、オペレーティングシステムが管理する実行の単位です。各スレッドは独自の実行スタックを持ち、CPUによって直接スケジュールされます。Goのゴルーチンは、最終的にOSスレッド上で実行されます。
init
関数
Go言語では、各パッケージにinit
関数を定義できます。この関数は、パッケージがインポートされた際に、main
関数が実行される前に自動的に一度だけ実行されます。init
関数は、パッケージ固有の初期化処理(例:設定の読み込み、データベース接続の確立など)を行うために使用されます。
runtime.LockOSThread()
runtime.LockOSThread()
は、現在のゴルーチンを現在のOSスレッドにロックするGoランタイム関数です。この関数が呼び出されると、そのゴルーチンは他のOSスレッドに移動することなく、ロックされたOSスレッド上で実行され続けます。これは、特定のOSスレッドに依存する処理(例:GUIイベントループ、C/C++ライブラリとの連携)を行う際に必要となることがあります。
スカベンジャーゴルーチン (Scavenger Goroutine)
Goランタイムには、メモリヒープのクリーンアップやOSへのメモリ解放を行うための「スカベンジャー」と呼ばれる特別なゴルーチンが存在します。これはバックグラウンドで動作し、システムのメモリ使用量を最適化する役割を担います。
技術的詳細
このコミットの主要な変更点は、Goプログラムの起動シーケンスにおけるinit
関数の実行タイミングと、スカベンジャーゴルーチンの起動タイミングの調整です。
以前は、スカベンジャーゴルーチンはruntime·schedinit
関数内で起動されていました。runtime·schedinit
はGoランタイムのスケジューラを初期化する関数であり、プログラムの初期段階で呼び出されます。しかし、この時点ではまだmain
関数が実行されておらず、init
関数も実行されていない可能性があります。
このコミットでは、スカベンジャーゴルーチンの起動をruntime·main
関数内に移動しています。runtime·main
関数は、Goプログラムのエントリポイントであり、すべてのinit
関数が実行された後に、ユーザーが記述したmain
関数を呼び出す直前に実行されます。
この変更により、以下の重要な効果が得られます。
-
init
関数のメインスレッド実行の保証:runtime·main
関数は、GoランタイムによってOSのメインスレッド上で実行されることが保証されています。スカベンジャーゴルーチンの起動をruntime·main
に移動し、main·init()
(ユーザーのinit
関数群を呼び出す部分)がruntime·main
内で実行されるようにすることで、すべてのinit
関数がメインスレッド上で実行されることが保証されます。これにより、init
関数内でruntime.LockOSThread()
を呼び出した際に、そのゴルーチンが確実にメインOSスレッドにロックされるようになります。 -
デッドロック検出ロジックの調整: スカベンジャーゴルーチンの起動タイミングが変更されたため、ランタイムのデッドロック検出ロジックも調整されています。Goランタイムは、すべてのゴルーチンがスリープ状態になり、実行可能なゴルーチンが存在しない場合にデッドロックと判断し、パニックを発生させます。以前のロジックでは、スカベンジャーゴルーチンが起動済みであることを前提としていましたが、新しいロジックではスカベンジャーゴルーチンがまだ起動していない可能性も考慮に入れ、より堅牢なデッドロック検出を行うようになっています。具体的には、
scvg == nil
(スカベンジャーゴルーチンがまだ存在しない)の場合と、スカベンジャーゴルーチンが存在し、それが唯一の実行中のゴルーチンである場合のデッドロックを区別して検出します。 -
テストの追加: この変更の正しさを検証するために、
runtime_linux_test.go
という新しいテストファイルが追加されています。このテストは、init
関数内でruntime.LockOSThread()
を呼び出し、そのゴルーチンがメインスレッド(pid
とtid
が一致することを確認)で実行されていることを検証します。
コアとなるコードの変更箇所
src/pkg/runtime/mheap.c
// Release (part of) unused memory to OS.
-// Goroutine created in runtime·schedinit.
+// Goroutine created at startup.
// Loop forever.
void
runtime·MHeap_Scavenger(void)
コメントの変更のみ。スカベンジャーゴルーチンがruntime·schedinit
で作成されるという記述から、より一般的な「起動時」に作成されるという記述に変更されています。
src/pkg/runtime/proc.c
@@ -209,8 +209,6 @@ runtime·schedinit(void)
mstats.enablegc = 1;
m->nomemprof--;
-
- scvg = runtime·newproc1((byte*)runtime·MHeap_Scavenger, nil, 0, 0, runtime·schedinit);
}
extern void main·init(void);
@@ -228,6 +226,7 @@ runtime·main(void)
// to preserve the lock.
runtime·LockOSThread();
runtime·sched.init = true;
+ scvg = runtime·newproc1((byte*)runtime·MHeap_Scavenger, nil, 0, 0, runtime·main);
main·init();
runtime·sched.init = false;
if(!runtime·sched.lockmain)
@@ -587,10 +586,11 @@ top:
mput(m);
}
-// Look for deadlock situation: one single active g which happens to be scvg.
- if(runtime·sched.grunning == 1 && runtime·sched.gwait == 0) {
- if(scvg->status == Grunning || scvg->status == Gsyscall)
- runtime·throw("all goroutines are asleep - deadlock!");
+// Look for deadlock situation.
+ if((scvg == nil && runtime·sched.grunning == 0) ||
+ (scvg != nil && runtime·sched.grunning == 1 && runtime·sched.gwait == 0 &&
+ (scvg->status == Grunning || scvg->status == Gsyscall))) {
+ runtime·throw("all goroutines are asleep - deadlock!");
}
m->nextg = nil;
runtime·schedinit
からスカベンジャーゴルーチン(scvg
)の起動コードが削除されました。runtime·main
関数内にスカベンジャーゴルーチンの起動コードが移動されました。これにより、main·init()
(ユーザーのinit
関数群の呼び出し)の直前にスカベンジャーゴルーチンが起動されるようになります。- デッドロック検出ロジックが変更されました。以前は、唯一のアクティブなゴルーチンがスカベンジャーゴルーチンである場合にデッドロックを検出していましたが、新しいロジックでは、スカベンジャーゴルーチンがまだ起動していない場合(
scvg == nil
)と、スカベンジャーゴルーチンが起動しており、それが唯一のアクティブなゴルーチンである場合の両方を考慮するようになりました。
src/pkg/runtime/runtime_linux_test.go
(新規ファイル)
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package runtime_test
import (
. "runtime"
"syscall"
"testing"
)
var pid, tid int
func init() {
// Record pid and tid of init thread for use during test.
// The call to LockOSThread is just to exercise it;
// we can't test that it does anything.
// Instead we're testing that the conditions are good
// for how it is used in init (must be on main thread).
pid, tid = syscall.Getpid(), syscall.Gettid()
LockOSThread()
}
func TestLockOSThread(t *testing.T) {
if pid != tid {
t.Fatalf("pid=%d but tid=%d", pid, tid)
}
}
runtime_linux_test.go
という新しいテストファイルが追加されました。- このテストファイル内の
init
関数で、現在のプロセスのPIDとスレッドのTIDを取得し、LockOSThread()
を呼び出しています。 TestLockOSThread
関数では、init
関数が実行されたスレッドのTIDがプロセスのPIDと一致することを確認しています。Linuxでは、メインスレッドのTIDはプロセスのPIDと同じになるため、これによりinit
関数がメインスレッドで実行されたことを検証しています。
コアとなるコードの解説
src/pkg/runtime/proc.c
の変更
-
スカベンジャーゴルーチンの移動:
runtime·schedinit
からscvg = runtime·newproc1(...)
の行が削除されました。これは、スケジューラの初期化時にスカベンジャーゴルーチンを起動するのではなく、より後の段階で起動するように変更されたことを意味します。runtime·main
関数内にscvg = runtime·newproc1(...)
の行が追加されました。runtime·main
はGoプログラムのメインエントリポイントであり、ユーザーのmain
関数が呼び出される前に実行されます。この変更により、スカベンジャーゴルーチンは、Goランタイムの初期化がより進んだ段階で、かつmain·init()
(すべてのinit
関数の実行)の直前に起動されることになります。これにより、init
関数が実行される際には、ランタイムの基本的なセットアップが完了しており、かつメインスレッド上で実行されるという前提が強化されます。
-
デッドロック検出ロジックの改善:
- デッドロック検出の条件が
if((scvg == nil && runtime·sched.grunning == 0) || ...)
に変更されました。 - 以前のロジックは、スカベンジャーゴルーチンが常に存在し、それが唯一のアクティブなゴルーチンである場合にデッドロックを検出していました。しかし、スカベンジャーゴルーチンの起動タイミングが変更されたため、
runtime·main
の初期段階ではscvg
がまだnil
である可能性があります。 - 新しいロジックでは、以下の2つのケースでデッドロックを検出します。
scvg == nil && runtime·sched.grunning == 0
: スカベンジャーゴルーチンがまだ起動しておらず、かつ実行中のゴルーチンが他に一つも存在しない場合。これは、プログラムが起動直後にデッドロック状態に陥ったことを意味します。scvg != nil && runtime·sched.grunning == 1 && runtime·sched.gwait == 0 && (scvg->status == Grunning || scvg->status == Gsyscall)
: スカベンジャーゴルーチンが起動しており、それが唯一の実行中のゴルーチンであり、かつ他のすべてのゴルーチンが待機状態にある場合。これは、従来のデッドロック検出ロジックと同じ意味合いです。
- この変更により、スカベンジャーゴルーチンの起動タイミングの変更に対応し、より正確なデッドロック検出が可能になります。
- デッドロック検出の条件が
src/pkg/runtime/runtime_linux_test.go
の新規追加
- このテストは、Goプログラムの
init
関数がOSのメインスレッドで実行されることを検証するために特別に設計されています。 init
関数内でsyscall.Getpid()
(プロセスID)とsyscall.Gettid()
(スレッドID)を呼び出し、これらの値をグローバル変数pid
とtid
に保存します。Linuxシステムでは、メインスレッドのTIDはプロセスのPIDと同じになります。LockOSThread()
がinit
関数内で呼び出されています。これは、このコミットの変更によってinit
関数がメインスレッドで実行されるようになったことを前提として、その動作を検証するためのものです。TestLockOSThread
関数では、pid
とtid
が等しいことをアサートしています。もしinit
関数がメインスレッド以外のスレッドで実行された場合、tid
はpid
と異なる値になるため、テストは失敗します。これにより、init
関数がメインスレッドで実行されるという保証が機能していることを確認できます。
関連リンク
参考にした情報源リンク
- Go runtime init on main threadに関するWeb検索結果
- Go言語の
init
関数に関するドキュメントやチュートリアル - Go言語のランタイムスケジューラに関する資料
runtime.LockOSThread()
に関するGoの公式ドキュメントや関連する議論- Goのデッドロック検出メカニズムに関する情報