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

[インデックス 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 rungo 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.initfalseに設定され、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

このファイルでは、デッドロックテストのフレームワークが大幅に改善されました。

  1. 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)を回避しています。

  2. 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が最新であることを確認します。これにより、テストが古いランタイムのバグを誤って報告するのを防ぎ、テストの信頼性を高めます。

  3. 新しいデッドロックテストケースの追加:

    • 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.csrc/pkg/runtime/crash_test.goの変更履歴)
  • Goのselectステートメントに関するドキュメント
  • Goのruntime.LockOSThread()に関するドキュメント
  • Goのgo runおよびgo installコマンドに関するドキュメント