[インデックス 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(¬e)
:Note
をクリアし、待機状態を解除します。runtime·notesleep(¬e)
: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を実行するように変更されました。
-
forcegchelper
関数の導入:static void forcegchelper(Note *note) { runtime·gc(1); runtime·notewakeup(note); }
この新しい関数は、実際に
runtime·gc(1)
を呼び出し、GCが完了した後にNote
を通知して、呼び出し元のゴルーチン(スカベンジャー)をウェイクアップします。 -
runtime·MHeap_Scavenger
内の変更:// runtime·MHeap_Scavenger 内 runtime·unlock(h); // ヒープのロックを解放 // 新しいゴルーチンでGCを実行 runtime·noteclear(¬e); // Noteをクリア notep = ¬e; // forcegchelperを新しいゴルーチンとして起動 runtime·newproc1((byte*)forcegchelper, (byte*)¬ep, sizeof(notep), 0, runtime·MHeap_Scavenger); // システムコールに入り、GCゴルーチンが完了するまで待機 runtime·entersyscall(); runtime·notesleep(¬e); // Noteが通知されるまでスカベンジャーはスリープ runtime·exitsyscall(); runtime·lock(h); // ヒープのロックを再取得
- スカベンジャーはまずヒープのロックを解放します。
- 次に、
forcegchelper
を新しいゴルーチンとして起動します。この新しいゴルーチンがruntime·gc(1)
を実行します。 - スカベンジャー自身は、
runtime·entersyscall()
を呼び出してシステムコール状態に入り、runtime·notesleep(¬e)
でスリープします。これにより、スカベンジャーは「ブロックされている」状態ではなく、「システムコール中でスリープしている」状態と見なされます。 forcegchelper
ゴルーチンがGCを完了すると、runtime·notewakeup(note)
を呼び出してスカベンジャーをウェイクアップさせます。- スカベンジャーは
runtime·exitsyscall()
を呼び出してシステムコール状態から抜け出し、ヒープのロックを再取得して処理を続行します。
この変更により、スカベンジャーゴルーチンはGCの実行中に直接runtime·worldsema
を待つことがなくなります。代わりに、GCは別のゴルーチンで実行され、スカベンジャーはNote
を介してその完了を待つ形になります。runtime·entersyscall()
とruntime·exitsyscall()
の使用は、ゴルーチンがシステムコール中にブロックされていることをランタイムに明示的に伝え、デッドロック検出器がそのゴルーチンをデッドロックの一部として誤ってカウントしないようにします。
これにより、デッドロック検出器が「偽のデッドロッククラッシュ」を引き起こす可能性が排除され、ランタイムの安定性が向上します。
コアとなるコードの変更箇所
src/pkg/runtime/mheap.c
ファイルの変更点:
-
forcegchelper
関数の追加:+static void +forcegchelper(Note *note) +{ + runtime·gc(1); + runtime·notewakeup(note); +}
-
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(¬e);\n+\t\t\tnotep = ¬e;\n+\t\t\truntime·newproc1((byte*)forcegchelper, (byte*)¬ep, sizeof(notep), 0, runtime·MHeap_Scavenger);\n+\t\t\truntime·entersyscall();\n+\t\t\truntime·notesleep(¬e);\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(¬e);
:Note
変数を初期化し、以前の状態をクリアします。これにより、新しい待機サイクルが開始されます。notep = ¬e;
:forcegchelper
に渡すNote
へのポインタを設定します。runtime·newproc1((byte*)forcegchelper, (byte*)¬ep, sizeof(notep), 0, runtime·MHeap_Scavenger);
:runtime·newproc1
は、新しいゴルーチンを作成し、実行を開始するためのランタイム内部関数です。- 第一引数には、新しいゴルーチンが実行する関数のポインタ(
forcegchelper
)を渡します。 - 第二引数以降は、
forcegchelper
に渡す引数(notep
)とそのサイズ、スタックサイズ、呼び出し元のゴルーチン(runtime·MHeap_Scavenger
)のポインタなどを指定します。 - これにより、
forcegchelper
が別のゴルーチンとしてバックグラウンドで実行され、スカベンジャーゴルーチンはブロックされずに済みます。
runtime·entersyscall();
: スカベンジャーゴルーチンがシステムコール状態に入ったことをランタイムに通知します。これにより、デッドロック検出器は、このゴルーチンが意図的にブロックされている(システムコールを待っている)と認識し、デッドロックの誤検知を防ぎます。runtime·notesleep(¬e);
: スカベンジャーゴルーチンは、forcegchelper
ゴルーチンがGCを完了し、note
を通知するまでここでスリープします。runtime·exitsyscall();
: スカベンジャーゴルーチンがシステムコール状態から抜け出したことをランタイムに通知します。runtime·lock(h);
: GCが完了し、スカベンジャーがウェイクアップした後、ヒープのロックを再取得します。
この一連の変更により、スカベンジャーゴルーチンはGCの実行中に直接ブロックされることなく、GCを別のゴルーチンに委譲し、その完了を安全に待つことができるようになります。これにより、デッドロック検出器の誤作動が回避されます。
関連リンク
- Go Issue #4243: https://github.com/golang/go/issues/4243
参考にした情報源リンク
- Goのソースコード(特に
src/runtime/mheap.go
やsrc/runtime/proc.go
など、ランタイムの内部実装に関するファイル) - Goのガベージコレクションに関する公式ドキュメントやブログ記事
- Goのスケジューラとゴルーチンに関する技術解説
- GoのIssueトラッカー(#4243)
- Goのコードレビューツール(
golang.org/cl/6682050
) - Goのランタイム内部に関する一般的な知識