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

[インデックス 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関数を呼び出す直前に実行されます。

この変更により、以下の重要な効果が得られます。

  1. init関数のメインスレッド実行の保証: runtime·main関数は、GoランタイムによってOSのメインスレッド上で実行されることが保証されています。スカベンジャーゴルーチンの起動をruntime·mainに移動し、main·init()(ユーザーのinit関数群を呼び出す部分)がruntime·main内で実行されるようにすることで、すべてのinit関数がメインスレッド上で実行されることが保証されます。これにより、init関数内でruntime.LockOSThread()を呼び出した際に、そのゴルーチンが確実にメインOSスレッドにロックされるようになります。

  2. デッドロック検出ロジックの調整: スカベンジャーゴルーチンの起動タイミングが変更されたため、ランタイムのデッドロック検出ロジックも調整されています。Goランタイムは、すべてのゴルーチンがスリープ状態になり、実行可能なゴルーチンが存在しない場合にデッドロックと判断し、パニックを発生させます。以前のロジックでは、スカベンジャーゴルーチンが起動済みであることを前提としていましたが、新しいロジックではスカベンジャーゴルーチンがまだ起動していない可能性も考慮に入れ、より堅牢なデッドロック検出を行うようになっています。具体的には、scvg == nil(スカベンジャーゴルーチンがまだ存在しない)の場合と、スカベンジャーゴルーチンが存在し、それが唯一の実行中のゴルーチンである場合のデッドロックを区別して検出します。

  3. テストの追加: この変更の正しさを検証するために、runtime_linux_test.goという新しいテストファイルが追加されています。このテストは、init関数内でruntime.LockOSThread()を呼び出し、そのゴルーチンがメインスレッド(pidtidが一致することを確認)で実行されていることを検証します。

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

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つのケースでデッドロックを検出します。
      1. scvg == nil && runtime·sched.grunning == 0: スカベンジャーゴルーチンがまだ起動しておらず、かつ実行中のゴルーチンが他に一つも存在しない場合。これは、プログラムが起動直後にデッドロック状態に陥ったことを意味します。
      2. 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)を呼び出し、これらの値をグローバル変数pidtidに保存します。Linuxシステムでは、メインスレッドのTIDはプロセスのPIDと同じになります。
  • LockOSThread()init関数内で呼び出されています。これは、このコミットの変更によってinit関数がメインスレッドで実行されるようになったことを前提として、その動作を検証するためのものです。
  • TestLockOSThread関数では、pidtidが等しいことをアサートしています。もしinit関数がメインスレッド以外のスレッドで実行された場合、tidpidと異なる値になるため、テストは失敗します。これにより、init関数がメインスレッドで実行されるという保証が機能していることを確認できます。

関連リンク

参考にした情報源リンク

  • Go runtime init on main threadに関するWeb検索結果
  • Go言語のinit関数に関するドキュメントやチュートリアル
  • Go言語のランタイムスケジューラに関する資料
  • runtime.LockOSThread()に関するGoの公式ドキュメントや関連する議論
  • Goのデッドロック検出メカニズムに関する情報