[インデックス 15325] ファイルの概要
このコミットは、Goランタイムにおけるデッドロック検出器の誤検知(false negative)を修正するものです。具体的には、特定の状況下で発生するデッドロックを検出できない問題を解決し、デッドロックテストの信頼性を向上させています。
コミット
commit 06a488fa97445414b727c5a7f1825e81e6c671ea
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Feb 20 12:15:02 2013 +0400
runtime: fix deadlock detector false negative
Fixes #4819.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7322086
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/06a488fa97445414b727c5a7f1825e81e6c671ea
元コミット内容
runtime: fix deadlock detector false negative
Fixes #4819.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7322086
変更の背景
Goランタイムには、すべてのゴルーチンがブロックされ、実行可能なゴルーチンが存在しない場合に発生するデッドロックを検出するメカニズムが組み込まれています。しかし、このデッドロック検出器には「false negative」、つまり実際にデッドロックが発生しているにもかかわらず、それを検出できないという問題が存在していました。
特に、func main() { select{} }
のような単純なプログラムでさえ、デッドロックが正しく検出されないケースがありました。これは、ランタイムの初期化プロセスとデッドロック検出器の動作タイミングに起因するものでした。デッドロック検出器が起動する前に、一部のゴルーチン(特にスカベンジャーゴルーチン)が起動し、その結果、システムが完全にアイドル状態ではないと誤認され、デッドロックが検出されないという状況が発生していました。
この問題は、GoのIssue #4819として報告されており、このコミットはその問題を解決するために導入されました。デッドロックの誤検知は、開発者がプログラムのバグを見逃す原因となるため、ランタイムの信頼性を高める上で重要な修正でした。
前提知識の解説
Goランタイム (Go Runtime)
Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ゴルーチン(軽量スレッド)のスケジューリング、メモリ管理(ガベージコレクション)、チャネル通信、デッドロック検出などが含まれます。Goプログラムは、OSのネイティブスレッド上でGoランタイムによって管理されるゴルーチンとして実行されます。
ゴルーチン (Goroutine)
ゴルーチンは、Goにおける並行処理の基本単位です。OSスレッドよりもはるかに軽量で、数千から数百万のゴルーチンを同時に実行できます。ゴルーチンはGoランタイムによってスケジューリングされ、必要に応じてOSスレッドにマッピングされます。
デッドロック (Deadlock)
デッドロックとは、複数の並行プロセス(この場合はゴルーチン)が互いに相手が保持しているリソースの解放を待ち続け、結果としてどのプロセスも先に進めなくなる状態を指します。Goにおいては、すべてのゴルーチンがブロックされ、実行可能なゴルーチンが一つも存在しない場合にデッドロックと判断されます。
デッドロック検出器 (Deadlock Detector)
Goランタイムには、プログラムがデッドロック状態に陥ったことを検出し、fatal error: all goroutines are asleep - deadlock!
のようなメッセージを出力してプログラムを終了させる機能があります。これは、開発者がデッドロックバグを特定するのに役立ちます。
select {}
Goのselect
ステートメントは、複数のチャネル操作を待機するために使用されます。select {}
のようにケースが一つも指定されていないselect
ステートメントは、どのチャネル操作も発生しないため、そのゴルーチンを永久にブロックします。これは意図的にデッドロックを発生させるテストケースや、プログラムがこれ以上実行すべきことがないことを示すために使用されることがあります。
runtime.LockOSThread()
runtime.LockOSThread()
関数は、現在のゴルーチンを現在のOSスレッドにロックします。これにより、そのゴルーチンは他のOSスレッドに移動したり、他のゴルーチンがそのOSスレッド上で実行されたりすることがなくなります。これは、特定のOSスレッドに依存するCgoコードや、OSスレッドのプロパティ(例: スレッドローカルストレージ)を利用する際に必要となることがあります。
go run
と go install runtime
go run
: Goソースファイルをコンパイルして実行します。通常、依存するパッケージはキャッシュされたバージョンを使用します。go install runtime
: Goの標準ライブラリの一部であるruntime
パッケージを再コンパイルし、インストールします。go run
が古いruntime.a
(コンパイル済みランタイムライブラリ)を使用している場合、最新のランタイムの変更が反映されないことがあります。このコミットのテストコードでは、go run
が古いruntime.a
を使用していないことを確認するためにgo list -f "{{.Stale}}" runtime
コマンドが追加されています。
技術的詳細
このコミットの主要な変更点は、Goランタイムのデッドロック検出ロジックのタイミング調整と、デッドロックテストの拡充です。
デッドロック検出の誤検知問題
Goランタイムの起動時、runtime·main
関数が実行されます。この中で、ガベージコレクションのスカベンジャーゴルーチン(scvg
)が起動されます。以前のコードでは、main·init()
(ユーザープログラムのinit
関数)が呼び出される前にruntime·sched.init
がfalse
に設定され、runtime·unlockOSThread()
が呼び出されていました。
問題は、main·init()
が呼び出される前にデッドロック検出器が「すべてのゴルーチンがスリープ状態である」と誤認する可能性があったことです。特に、func main() { select{} }
のようなプログラムでは、main·init()
が実行される前にmain
ゴルーチンがselect{}
でブロックされ、スカベンジャーゴルーチンがまだ完全に起動していないか、またはデッドロック検出器がその存在を適切に考慮できていない場合に、デッドロックが検出されないという「false negative」が発生していました。
修正内容
このコミットでは、src/pkg/runtime/proc.c
内のruntime·main
関数における処理順序が変更されました。
変更前:
// ...
main·init();
runtime·sched.init = false;
runtime·unlockOSThread();
// The deadlock detection has false negatives.
// Let scvg start up, to eliminate the false negative
// for the trivial program func main() { select{} }.
runtime·gosched();
// ...
変更後:
// ...
// The deadlock detection has false negatives.
// Let scvg start up, to eliminate the false negative
// for the trivial program func main() { select{} }.
runtime·gosched();
main·init();
runtime·sched.init = false;
runtime·unlockOSThread();
// ...
この変更により、runtime·gosched()
(ゴルーチンをスケジューリングし、他のゴルーチンに実行を譲る)がmain·init()
の呼び出しとruntime·sched.init
の更新の前に移動されました。これにより、スカベンジャーゴルーチンが十分に起動し、デッドロック検出器がシステムの状態をより正確に評価できるようになります。結果として、func main() { select{} }
のような単純なデッドロックも正しく検出されるようになります。
テストの拡充
src/pkg/runtime/crash_test.go
には、デッドロック検出のテストケースが大幅に追加されました。
testDeadlock
ヘルパー関数が導入され、デッドロックを期待するテストの共通ロジックをカプセル化しています。TestSimpleDeadlock
:select {}
のみを含む最も単純なデッドロックをテストします。TestInitDeadlock
:init()
関数内でselect {}
によってデッドロックが発生するケースをテストします。TestLockedDeadlock
およびTestLockedDeadlock2
:runtime.LockOSThread()
を使用してOSスレッドにゴルーチンをロックした状態でデッドロックが発生するケースをテストします。これは、Cgoとの連携や特定のOSスレッド依存のシナリオでデッドロックが発生する可能性をカバーします。
また、crash_test.go
には、go run
が古いruntime.a
を使用していないことを確認するためのcheckStaleRuntime
関数が追加されました。これは、テストの信頼性を高めるための重要な変更です。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルは以下の通りです。
src/pkg/runtime/crash_cgo_test.go
: Cgo関連のクラッシュハンドラーテストの呼び出し方法が変更されました。crashTest
構造体を直接渡すのではなく、cgo
ブール値を直接渡すように簡素化されています。src/pkg/runtime/crash_test.go
: デッドロック検出のテストケースが大幅に拡充されました。新しいデッドロックテスト関数と、テスト実行環境の健全性をチェックするロジックが追加されています。src/pkg/runtime/proc.c
: Goランタイムのメイン処理を行うファイルで、デッドロック検出器の誤検知を修正するために、ランタイム初期化の順序が変更されました。
コアとなるコードの解説
src/pkg/runtime/proc.c
// 変更前
// ...
// The deadlock detection has false negatives.
// Let scvg start up, to eliminate the false negative
// for the trivial program func main() { select{} }.
// runtime·gosched(); // この行は以前は main·init() の後にあった
main·init();
runtime·sched.init = false;
runtime·unlockOSThread();
// 変更後
// ...
// The deadlock detection has false negatives.
// Let scvg start up, to eliminate the false negative
// for the trivial program func main() { select{} }.
runtime·gosched(); // この行が main·init() の前に移動
main·init();
runtime·sched.init = false;
runtime·unlockOSThread();
この変更は、runtime·gosched()
の呼び出しをmain·init()
の前に移動させることで、スカベンジャーゴルーチン(scvg
)が十分に起動し、デッドロック検出器がシステムの状態をより正確に評価できるようにすることを目的としています。これにより、func main() { select{} }
のような単純なデッドロックも正しく検出されるようになります。
src/pkg/runtime/crash_test.go
このファイルでは、デッドロックテストのフレームワークが大幅に改善されました。
-
executeTest
関数の導入:func executeTest(t *testing.T, templ string, data interface{}) string { checkStaleRuntime(t) // 新しく追加されたランタイムの鮮度チェック // ... cmd := exec.Command("go", "run", src) for _, s := range os.Environ() { if strings.HasPrefix(s, "GOMAXPROCS") { continue } cmd.Env = append(cmd.Env, s) } got, _ := cmd.CombinedOutput() return string(got) }
この関数は、テスト対象のGoプログラムを一時ファイルに書き込み、
go run
で実行し、その出力を返します。特に、GOMAXPROCS
環境変数をクリアして実行することで、デッドロックテストがGOMAXPROCS > 1
の場合にハングする問題(Issue 4826)を回避しています。 -
checkStaleRuntime
関数の導入:func checkStaleRuntime(t *testing.T) { out, err := exec.Command("go", "list", "-f", "{{.Stale}}", "runtime").CombinedOutput() if err != nil { t.Fatalf("failed to execute 'go list': %v\n%v", err, string(out)) } if string(out) != "false\n" { t.Fatalf("Stale runtime.a. Run 'go install runtime'.") } }
この関数は、
go run
が使用するruntime.a
が最新であることを確認します。これにより、テストが古いランタイムのバグを誤って報告するのを防ぎ、テストの信頼性を高めます。 -
新しいデッドロックテストケースの追加:
simpleDeadlockSource
:
最も基本的なデッドロックケース。package main func main() { select {} }
initDeadlockSource
:package main func init() { select {} } func main() { }
init
関数内でのデッドロックケース。lockedDeadlockSource
/lockedDeadlockSource2
:package main import "runtime" func main() { runtime.LockOSThread() select {} }
runtime.LockOSThread()
を使用したデッドロックケース。これは、特定のOSスレッドにゴルーチンがロックされている状況でのデッドロック検出をテストします。
これらのテストケースは、デッドロック検出器が様々なシナリオで正しく機能することを確認するために不可欠です。
関連リンク
- Go Gerrit Change-ID: https://golang.org/cl/7322086
- Go Issue #4819 (コミットメッセージに記載されているが、直接のGitHubリンクは不明)
参考にした情報源リンク
- Go言語の公式ドキュメント (Go Runtime, Goroutines, Deadlockに関する一般的な情報)
- GoのIssueトラッカー (Issue #4819の具体的な内容を特定するため)
- Goのソースコード (特に
src/pkg/runtime/proc.c
とsrc/pkg/runtime/crash_test.go
の変更履歴) - Goの
select
ステートメントに関するドキュメント - Goの
runtime.LockOSThread()
に関するドキュメント - Goの
go run
およびgo install
コマンドに関するドキュメント