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

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

このコミットは、Goランタイムにおける潜在的なガベージコレクション(GC)のデッドロックを修正するものです。具体的には、GC処理中にメモリ割り当てが行われる際に、GCが一時的に無効化されるようにm->locksカウンタを操作することで、競合状態を防ぎます。

コミット

  • コミットハッシュ: 902911bcff91127cd8c5c7e10d200eb97f0c1893
  • Author: Dmitriy Vyukov dvyukov@google.com
  • Date: Mon Jun 25 11:08:09 2012 +0400
  • コミットメッセージ:
    runtime: fix potential GC deadlock
    The issue seems to not be triggered right now,
    but I've seen the deadlock after some other legal
    modifications to runtime.
    So I think we are safer this way.
    
    R=rsc, r
    CC=golang-dev
    https://golang.org/cl/6339051
    

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

https://github.com/golang/go/commit/902911bcff91127cd8c5c7e10d200eb97f0c1893

元コミット内容

runtime: fix potential GC deadlock
The issue seems to not be triggered right now,
but I've seen the deadlock after some other legal
modifications to runtime.
So I think we are safer this way.

R=rsc, r
CC=golang-dev
https://golang.org/cl/6339051

変更の背景

この変更は、Goランタイムのガベージコレクション(GC)プロセスにおける潜在的なデッドロックを解消するために導入されました。コミットメッセージによると、このデッドロックは現在のコードベースでは直接トリガーされないものの、ランタイムに対する他の正当な変更を加えた後に発生する可能性があることがDmitriy Vyukov氏によって確認されています。これは、Goランタイムのような低レベルの並行処理システムでは、一見無関係な変更が既存の同期メカニズムに予期せぬ影響を与え、競合状態やデッドロックを引き起こす可能性があることを示唆しています。そのため、将来的な安定性と堅牢性を確保するために、この予防的な修正が適用されました。

前提知識の解説

Goのガベージコレクション (GC)

Go言語は、自動メモリ管理のためにガベージコレクタ(GC)を採用しています。GoのGCは、主に並行マーク&スイープ方式で動作し、プログラムの実行と並行して不要なメモリを回収します。GCサイクル中には、一時的にすべてのゴルーチン(Goの軽量スレッド)の実行を停止する「Stop-the-World (STW)」フェーズが存在します。このSTWフェーズは、GCがメモリの状態を正確に把握し、一貫性を保つために不可欠ですが、その時間は最小限に抑えるように設計されています。

デッドロック

デッドロックとは、複数のプロセスやスレッドが互いに相手が保持しているリソースの解放を待ち続け、結果としてどのプロセスも処理を進められなくなる状態を指します。並行プログラミングにおいて、共有リソースへのアクセスを同期するためにロック(ミューテックスなど)が使用されますが、ロックの取得順序の不一致や、ロックの解放忘れなどによってデッドロックが発生する可能性があります。Goランタイムのような複雑なシステムでは、内部的なロックメカニズムが多数存在し、それらの相互作用によってデッドロックが発生することがあります。

m.locks と Goランタイムの M (Machine) 構造体

Goランタイムの内部には、OSスレッドを表すM(Machine)という構造体が存在します。このM構造体には、locksというフィールドが含まれています。m.locksは、そのOSスレッドが現在保持しているランタイム内部のロックの数を追跡するためのカウンタです。このカウンタは、特定の重要なランタイム操作(例えば、GCの実行中など)において、そのOSスレッドがGCによって停止されるべきではないことを示すために使用されることがあります。m.locksが0より大きい場合、そのOSスレッドはGCのSTWフェーズ中に停止されない、あるいは特定のGC関連の処理がブロックされる可能性があります。これは、GCがメモリを操作している最中に、別のスレッドがそのメモリを書き換えることを防ぐための重要な同期メカニズムの一部です。

技術的詳細

このコミットは、src/pkg/runtime/mgc0.cファイル内のruntime·gc関数、すなわちGoのガベージコレクションの主要なエントリポイントに修正を加えています。

問題の核心は、GCプロセス中にruntime·parforallocという関数が呼び出される点にあります。parforallocは、並行処理のためのメモリを割り当てる関数であり、この割り当て処理自体がGCの対象となる可能性のあるヒープメモリを操作します。

GCが実行されている最中に、GC自身がメモリを割り当てようとすると、GCの内部状態とメモリ割り当ての間に競合状態が発生し、デッドロックを引き起こす可能性があります。特に、GCがメモリをマークまたはスイープしている最中に、parforallocが新しいメモリを要求し、そのメモリがGCの現在のフェーズと矛盾する状態にある場合、問題が生じます。

この修正では、runtime·parforallocが呼び出される直前にm->locks++を実行し、その直後にm->locks--を実行しています。

  • m->locks++: これは、現在のOSスレッド(m)がランタイム内部のロックを一つ取得したことを示します。これにより、GCがこのスレッドを停止しようとしたり、このスレッドがメモリを割り当てる際にGCの特定の側面がブロックされたりする可能性があります。このコンテキストでは、「GC中にparforallocでのメモリ割り当てを無効にする」というコメントが示唆するように、GCがparforallocのメモリ割り当て処理と競合しないように、一時的にGCの特定の側面を抑制する効果があります。
  • m->locks--: parforallocの処理が完了した後、ロックを解放したことを示します。

この操作により、parforallocが実行されている間、GCは特定の制約の下で動作するか、あるいは一時的にそのスレッドに対するGCの介入が抑制されます。これにより、GCとメモリ割り当ての間のデッドロックの可能性が排除され、ランタイムの安定性が向上します。

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

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -905,12 +905,14 @@ runtime·gc(int32 force)
 	work.debugmarkdone = 0;
 	work.nproc = runtime·gcprocs();
 	addroots();
+\tm->locks++;	// disable gc during mallocs in parforalloc
 	if(work.markfor == nil)
 		work.markfor = runtime·parforalloc(MaxGcproc);
 	runtime·parforsetup(work.markfor, work.nproc, work.nroot, nil, false, markroot);
 	if(work.sweepfor == nil)
 		work.sweepfor = runtime·parforalloc(MaxGcproc);
 	runtime·parforsetup(work.sweepfor, work.nproc, runtime·mheap.nspan, nil, true, sweepspan);
+\tm->locks--;
 	if(work.nproc > 1) {
 		runtime·noteclear(&work.alldone);
 		runtime·helpgc(work.nproc);

コアとなるコードの解説

変更はsrc/pkg/runtime/mgc0.cファイルのruntime·gc関数内で行われています。

  1. m->locks++; // disable gc during mallocs in parforalloc:

    • この行は、runtime·parforallocが呼び出される直前に挿入されています。
    • mは現在のOSスレッド(Machine)を表すポインタです。
    • m->locksカウンタをインクリメントすることで、現在のスレッドがランタイム内部のロックを保持している状態にします。
    • コメントにあるように、これはparforalloc内でのメモリ割り当て(mallocs)中にGCが介入するのを一時的に「無効にする」ことを目的としています。これにより、GCがメモリをスキャンまたは操作している最中に、parforallocがそのメモリ構造を変更しようとすることによる競合状態やデッドロックを防ぎます。
  2. m->locks--;:

    • この行は、runtime·parforallocの呼び出し(work.markforwork.sweepforの両方に対する設定を含む)が完了した直後に挿入されています。
    • m->locksカウンタをデクリメントすることで、先ほど取得した「ロック」を解放します。
    • これにより、parforallocのクリティカルセクションが終了し、GCがこのスレッドに対して通常の動作を再開できるようになります。

このペアの操作は、parforallocが実行される間だけ、GCの特定の側面を抑制する非常に短い期間のクリティカルセクションを作成します。これは、GCとメモリ割り当ての間の同期を確保し、潜在的なデッドロックを回避するための重要なメカニズムです。

関連リンク

参考にした情報源リンク