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

[インデックス 14155] ファイルの概要

このコミットは、Goランタイムにおける「偽のデッドロッククラッシュ(spurious deadlock crashes)」を修正することを目的としています。具体的には、ガベージコレクタ(GC)のスカベンジャー(scavenger)ゴルーチンがGCを強制実行する際に、他のゴルーチンをブロックすることでデッドロック検出器が誤作動する問題を解決します。

コミット

commit f24323c93e524bfa7d24cd7dcea93c11b983d4d5
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Oct 16 14:41:32 2012 +0400

    runtime: fix spurious deadlock crashes
    Fixes #4243.
    
    R=golang-dev, iant
    CC=golang-dev, sebastien.paolacci
    https://golang.org/cl/6682050

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/f24323c93e524bfa7d24cd7dcea93c11b983d4d5

元コミット内容

runtime: fix spurious deadlock crashes
Fixes #4243.

変更の背景

Goランタイムには、メモリ管理とガベージコレクション(GC)を担当する「スカベンジャー」と呼ばれる特別なゴルーチンが存在します。このスカベンジャーは、一定期間GCが実行されない場合に、GCを強制的にトリガーする役割を担っています。

しかし、従来のGoランタイムでは、スカベンジャーがGCを強制実行する際に、runtime·gc(1)関数を直接呼び出していました。runtime·gc(1)は、GCの実行中に他のすべてのゴルーチンを停止させる("world stop")性質を持っています。この「world stop」は、runtime·worldsemaというセマフォを通じて実現されます。

問題は、Goランタイムにはデッドロックを検出するメカニズムも存在することです。デッドロック検出器は、ゴルーチンが長時間ブロックされている状態を監視し、デッドロックと判断した場合にプログラムをクラッシュさせることがあります。

スカベンジャーがruntime·gc(1)を直接呼び出すと、スカベンジャー自身がruntime·worldsemaを待つことになり、同時に他のゴルーチンも停止させられます。この状況下で、デッドロック検出器がスカベンジャーがブロックされていると誤って判断し、「偽のデッドロッククラッシュ」を引き起こす可能性がありました。つまり、実際にはデッドロックではないにもかかわらず、ランタイムがデッドロックと誤認して終了してしまうという問題です。

このコミットは、この「偽のデッドロッククラッシュ」を修正するために導入されました。関連するGoのIssueは #4243 です。

前提知識の解説

Goランタイムとガベージコレクション (GC)

Goは独自のランタイムを持ち、メモリ管理、ゴルーチン、スケジューラ、ガベージコレクションなどを担当します。GoのGCは並行・並行(concurrent and parallel)なマーク&スイープ方式を採用しており、ほとんどのGC作業はアプリケーションの実行と並行して行われます。しかし、GCの特定のフェーズ(特にマークフェーズの開始と終了)では、すべてのゴルーチンを一時的に停止させる「world stop」が必要になります。

スカベンジャーゴルーチン (runtime·MHeap_Scavenger)

runtime·MHeap_Scavengerは、Goランタイムのヒープ(MHeap)に関連する特別なゴルーチンです。主な役割は以下の通りです。

  • 未使用メモリのOSへの解放: ヒープ内で未使用になったメモリ領域をOSに返却します。
  • 強制GCのトリガー: 一定期間(このコミットの時点では2分)GCが実行されない場合に、GCを強制的にトリガーします。これは、アプリケーションがメモリをあまり割り当てないが、GCが必要な状況(例えば、多くのゴルーチンが長時間アイドル状態にある場合)に対応するためです。

mheap.c

mheap.cはGoランタイムのC言語で書かれた部分であり、主にメモリヒープの管理、アロケーション、デロケーション、そしてガベージコレクションに関連するロジックが含まれています。スカベンジャーゴルーチンの実装もこのファイルにあります。

Note 型とゴルーチン間の同期

Goランタイムの内部では、Note型は低レベルの同期プリミティブとして使用されます。これは、ゴルーチンが特定のイベントを待機したり、他のゴルーチンにイベントを通知したりするために使われます。

  • runtime·noteclear(&note): Noteをクリアし、待機状態を解除します。
  • runtime·notesleep(&note): Noteが通知されるまで現在のゴルーチンをスリープさせます。
  • runtime·notewakeup(note): Noteを待機しているゴルーチンをウェイクアップさせます。

runtime·worldsema

runtime·worldsemaは、GoランタイムがGCの「world stop」フェーズを調整するために使用するセマフォです。GCがworld stopを開始する際、このセマフォを獲得し、すべてのアプリケーションゴルーチンが停止するのを待ちます。GCがworld stopを終了すると、セマフォを解放し、アプリケーションゴルーチンが再開できるようにします。

デッドロック検出器

Goランタイムには、プログラムがデッドロック状態に陥ったことを検出するメカニズムがあります。これは、ゴルーチンが長時間ブロックされ、かつ他のゴルーチンも同様にブロックされていて、進行がない場合にデッドロックと判断し、プログラムを異常終了させることがあります。これは、開発者がデッドロックのバグを発見するのに役立ちますが、誤検知は避けなければなりません。

技術的詳細

このコミットの核心は、スカベンジャーゴルーチンがGCを強制実行する際のruntime·gc(1)の呼び出し方を変更した点にあります。

変更前: スカベンジャーゴルーチンは、GCが必要な場合に直接runtime·gc(1)を呼び出していました。

// runtime·MHeap_Scavenger 内
runtime·unlock(h);
runtime·gc(1); // ここでworld stopが発生し、スカベンジャー自身もブロックされる
runtime·lock(h);

この直接呼び出しは、スカベンジャー自身がruntime·worldsemaを待つことになり、デッドロック検出器がスカベンジャーをブロックされたゴルーチンとして誤って認識する原因となっていました。

変更後: スカベンジャーゴルーチンは、GCを直接呼び出すのではなく、新しいゴルーチンを生成してその中でGCを実行するように変更されました。

  1. forcegchelper関数の導入:

    static void
    forcegchelper(Note *note)
    {
        runtime·gc(1);
        runtime·notewakeup(note);
    }
    

    この新しい関数は、実際にruntime·gc(1)を呼び出し、GCが完了した後にNoteを通知して、呼び出し元のゴルーチン(スカベンジャー)をウェイクアップします。

  2. runtime·MHeap_Scavenger内の変更:

    // runtime·MHeap_Scavenger 内
    runtime·unlock(h); // ヒープのロックを解放
    
    // 新しいゴルーチンでGCを実行
    runtime·noteclear(&note); // Noteをクリア
    notep = &note;
    // forcegchelperを新しいゴルーチンとして起動
    runtime·newproc1((byte*)forcegchelper, (byte*)&notep, sizeof(notep), 0, runtime·MHeap_Scavenger);
    
    // システムコールに入り、GCゴルーチンが完了するまで待機
    runtime·entersyscall();
    runtime·notesleep(&note); // Noteが通知されるまでスカベンジャーはスリープ
    runtime·exitsyscall();
    
    runtime·lock(h); // ヒープのロックを再取得
    
    • スカベンジャーはまずヒープのロックを解放します。
    • 次に、forcegchelperを新しいゴルーチンとして起動します。この新しいゴルーチンがruntime·gc(1)を実行します。
    • スカベンジャー自身は、runtime·entersyscall()を呼び出してシステムコール状態に入り、runtime·notesleep(&note)でスリープします。これにより、スカベンジャーは「ブロックされている」状態ではなく、「システムコール中でスリープしている」状態と見なされます。
    • forcegchelperゴルーチンがGCを完了すると、runtime·notewakeup(note)を呼び出してスカベンジャーをウェイクアップさせます。
    • スカベンジャーはruntime·exitsyscall()を呼び出してシステムコール状態から抜け出し、ヒープのロックを再取得して処理を続行します。

この変更により、スカベンジャーゴルーチンはGCの実行中に直接runtime·worldsemaを待つことがなくなります。代わりに、GCは別のゴルーチンで実行され、スカベンジャーはNoteを介してその完了を待つ形になります。runtime·entersyscall()runtime·exitsyscall()の使用は、ゴルーチンがシステムコール中にブロックされていることをランタイムに明示的に伝え、デッドロック検出器がそのゴルーチンをデッドロックの一部として誤ってカウントしないようにします。

これにより、デッドロック検出器が「偽のデッドロッククラッシュ」を引き起こす可能性が排除され、ランタイムの安定性が向上します。

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

src/pkg/runtime/mheap.c ファイルの変更点:

  1. forcegchelper 関数の追加:

    +static void
    +forcegchelper(Note *note)
    +{
    +	runtime·gc(1);
    +	runtime·notewakeup(note);
    +}
    
  2. runtime·MHeap_Scavenger 内の runtime·gc(1) 呼び出しの変更:

    @@ -385,7 +392,15 @@ runtime·MHeap_Scavenger(void)\n \t\tnow = runtime·nanotime();\n \t\tif(now - mstats.last_gc > forcegc) {\n \t\t\truntime·unlock(h);\n-\t\t\t\truntime·gc(1);\n+\t\t\t// The scavenger can not block other goroutines,\n+\t\t\t// otherwise deadlock detector can fire spuriously.\n+\t\t\t// GC blocks other goroutines via the runtime·worldsema.\n+\t\t\truntime·noteclear(&note);\n+\t\t\tnotep = &note;\n+\t\t\truntime·newproc1((byte*)forcegchelper, (byte*)&notep, sizeof(notep), 0, runtime·MHeap_Scavenger);\n+\t\t\truntime·entersyscall();\n+\t\t\truntime·notesleep(&note);\n+\t\t\truntime·exitsyscall();\n \t\t\truntime·lock(h);\n \t\t\tnow = runtime·nanotime();\n \t\t\tif (trace)\n    ```
    
    

コアとなるコードの解説

forcegchelper 関数

この関数は、GCを実際に実行する新しいゴルーチンのエントリポイントとして機能します。

  • runtime·gc(1);: これが実際のGC呼び出しです。この関数が実行されると、GoランタイムはGCプロセスを開始し、必要に応じて「world stop」を実行します。
  • runtime·notewakeup(note);: GCが完了した後、この関数が呼び出され、noteを待機しているゴルーチン(この場合はスカベンジャーゴルーチン)をウェイクアップします。

runtime·MHeap_Scavenger 内の変更されたロジック

  • runtime·unlock(h);: GCを実行する前に、ヒープのロックを解放します。これは、GCがヒープにアクセスする必要があるためです。
  • runtime·noteclear(&note);: Note変数を初期化し、以前の状態をクリアします。これにより、新しい待機サイクルが開始されます。
  • notep = &note;: forcegchelperに渡すNoteへのポインタを設定します。
  • runtime·newproc1((byte*)forcegchelper, (byte*)&notep, sizeof(notep), 0, runtime·MHeap_Scavenger);:
    • runtime·newproc1は、新しいゴルーチンを作成し、実行を開始するためのランタイム内部関数です。
    • 第一引数には、新しいゴルーチンが実行する関数のポインタ(forcegchelper)を渡します。
    • 第二引数以降は、forcegchelperに渡す引数(notep)とそのサイズ、スタックサイズ、呼び出し元のゴルーチン(runtime·MHeap_Scavenger)のポインタなどを指定します。
    • これにより、forcegchelperが別のゴルーチンとしてバックグラウンドで実行され、スカベンジャーゴルーチンはブロックされずに済みます。
  • runtime·entersyscall();: スカベンジャーゴルーチンがシステムコール状態に入ったことをランタイムに通知します。これにより、デッドロック検出器は、このゴルーチンが意図的にブロックされている(システムコールを待っている)と認識し、デッドロックの誤検知を防ぎます。
  • runtime·notesleep(&note);: スカベンジャーゴルーチンは、forcegchelperゴルーチンがGCを完了し、noteを通知するまでここでスリープします。
  • runtime·exitsyscall();: スカベンジャーゴルーチンがシステムコール状態から抜け出したことをランタイムに通知します。
  • runtime·lock(h);: GCが完了し、スカベンジャーがウェイクアップした後、ヒープのロックを再取得します。

この一連の変更により、スカベンジャーゴルーチンはGCの実行中に直接ブロックされることなく、GCを別のゴルーチンに委譲し、その完了を安全に待つことができるようになります。これにより、デッドロック検出器の誤作動が回避されます。

関連リンク

参考にした情報源リンク

  • Goのソースコード(特にsrc/runtime/mheap.gosrc/runtime/proc.goなど、ランタイムの内部実装に関するファイル)
  • Goのガベージコレクションに関する公式ドキュメントやブログ記事
  • Goのスケジューラとゴルーチンに関する技術解説
  • GoのIssueトラッカー(#4243)
  • Goのコードレビューツール(golang.org/cl/6682050
  • Goのランタイム内部に関する一般的な知識