[インデックス 15851] ファイルの概要
このコミットは、Goランタイムにおけるハッシュマップ挿入時のガベージコレクション(GC)の挙動に関するバグ修正(Issue #5074の修正の第2弾)を目的としています。特にマルチスレッド環境下での問題に対処し、GCがハッシュマップの挿入処理中に発生することによって引き起こされる可能性のある競合状態やデッドロックを防ぐための変更が加えられています。具体的には、runtime/malloc.goc
内のメモリ割り当て関数runtime·mallocgc
において、GC待機中にスケジューリングを行う条件にdogc
(GCを行う必要があるかを示すフラグ)が追加されました。
コミット
commit 2001f0c28ef4f2b7b907d060901a6fad2f1e9eb0
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date: Wed Mar 20 20:36:33 2013 +0100
runtime: prevent garbage collection during hashmap insertion (fix 2)
Fixes #5074 in multi-threaded scenarios.
R=golang-dev, daniel.morsing, dave, dvyukov, bradfitz, rsc
CC=golang-dev, remyoudompheng
https://golang.org/cl/7916043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2001f0c28ef4f2b7b907d060901a6fad2f1e9eb0
元コミット内容
このコミットの元々の内容は、「runtime: prevent garbage collection during hashmap insertion (fix 2)」であり、マルチスレッド環境におけるIssue #5074の修正を意図しています。これは、ハッシュマップへの要素挿入中にガベージコレクションが実行されることによって発生する潜在的な問題を解決するためのものです。
変更の背景
Goランタイムでは、メモリ割り当てとガベージコレクションが密接に連携しています。特に、ハッシュマップ(Goのmap
型)のようなデータ構造は、要素の挿入時に内部的にメモリを割り当てることがあります。マルチスレッド環境、すなわち複数のゴルーチンが同時に動作している状況では、あるゴルーチンがハッシュマップに要素を挿入している最中に、別のゴルーチンがGCをトリガーしたり、GCが実行されたりすると、競合状態(race condition)やデッドロックが発生する可能性がありました。
Issue #5074は、このような特定のシナリオ、特にハッシュマップの挿入中にGCが介入することで、プログラムがハングアップしたり、不正な状態になったりする問題が報告されたものと考えられます。このコミットは、その問題に対する2番目の修正("fix 2")であり、以前の修正ではカバーしきれなかったマルチスレッド環境でのエッジケースに対応することを目的としています。
前提知識の解説
Goのガベージコレクション (GC)
Goのガベージコレクションは、主に以下の特徴を持ちます。
- 並行GC (Concurrent GC): GoのGCは、ほとんどの作業をアプリケーションの実行と並行して行います。これにより、アプリケーションの一時停止時間(Stop-The-World: STW)を最小限に抑え、レイテンシを低減します。
- Stop-The-World (STW): 並行GCであっても、GCの特定のフェーズ(例えば、マークフェーズの開始やスイープフェーズの終了など)では、すべてのゴルーチンを一時的に停止させる必要があります。この一時停止期間がSTWです。STW中は、アプリケーションの実行が完全に停止します。
- GCトリガー: GCは、ヒープの使用量が一定の閾値を超えた場合や、
runtime.GC()
が明示的に呼び出された場合などにトリガーされます。
Goのハッシュマップ (map
)
Goのmap
型は、キーと値のペアを格納する組み込みのデータ構造です。内部的にはハッシュテーブルとして実装されており、要素の挿入、削除、検索が平均O(1)の時間計算量で行えます。
- メモリ割り当て:
map
に新しい要素を挿入する際、必要に応じて内部のバケット(ハッシュテーブルの配列要素)を拡張するために、新しいメモリが割り当てられることがあります。このメモリ割り当ては、Goランタイムのmallocgc
関数を通じて行われます。 - 並行アクセス: Goの
map
は、複数のゴルーチンからの同時書き込みアクセスに対して安全ではありません。複数のゴルーチンが同時にmap
に書き込もうとすると、データ競合が発生し、パニックを引き起こす可能性があります。このため、並行アクセスが必要な場合は、sync.Mutex
などの同期プリミティブを使用するか、sync.Map
のような並行マップの実装を使用する必要があります。
マルチスレッドとゴルーチン
Goでは、OSのスレッドを直接扱う代わりに「ゴルーチン(goroutine)」という軽量な並行処理の単位を使用します。
- ゴルーチン: ゴルーチンは、Goランタイムによって管理される軽量なスレッドのようなものです。数千、数万のゴルーチンを同時に実行しても、OSのスレッドのようにリソースを大量に消費することはありません。
- M (Machine) と P (Processor) と G (Goroutine): Goランタイムのスケジューラは、M(OSスレッド)、P(論理プロセッサ)、G(ゴルーチン)という3つのエンティティを使って並行処理を管理します。
G
: 実行されるゴルーチン。P
: ゴルーチンを実行するためのコンテキスト。Goのコードを実行するために必要なリソース(スケジューラのキュー、メモリ割り当てキャッシュなど)を保持します。M
: OSスレッド。Pにアタッチされ、P上でGを実行します。
m->g0
: 各M(OSスレッド)には、特別なゴルーチンであるg0
が関連付けられています。g0
は、ランタイムの内部処理(スケジューリング、GC、システムコールなど)を実行するために使用されるスタックを持つゴルーチンです。通常のアプリケーションゴルーチン(g
)とは異なり、g0
はGCの対象外であり、GCのSTWフェーズ中でも実行され続けることができます。m->locks
: これは、現在のM(OSスレッド)が保持しているロックの数を追跡するカウンタです。ランタイム内部でロックが取得されるとインクリメントされ、解放されるとデクリメントされます。このカウンタは、GCが実行されるべきではないクリティカルセクションにいるかどうかを判断するために使用されます。
技術的詳細
このコミットの核心は、runtime·mallocgc
関数内の条件式に&& dogc
を追加した点にあります。
元のコード:
if(runtime·gcwaiting && g != m->g0 && m->locks == 0)
runtime·gosched();
変更後のコード:
if(runtime·gcwaiting && g != m->g0 && m->locks == 0 && dogc)
runtime·gosched();
この条件式は、メモリ割り当て(mallocgc
)が行われる際に、現在のゴルーチンを一時停止してスケジューラに制御を戻す(runtime·gosched()
)べきかどうかを判断しています。これは、GCが進行中である場合に、アプリケーションゴルーチンがGCの完了を待つためのメカニズムの一部です。
各条件の意味は以下の通りです。
runtime·gcwaiting
: これは、GCが現在待機状態にあることを示すグローバルフラグです。GCがSTWフェーズに入ろうとしているか、または既にSTWフェーズに入っており、すべてのアプリケーションゴルーチンが停止するのを待っている状態を示します。g != m->g0
: 現在実行中のゴルーチンが、ランタイムの内部処理用ゴルーチンであるg0
ではないことを確認します。g0
はGCの対象外であり、GCのSTW中でも実行され続ける必要があるため、g0
の場合はスケジューリングを行いません。m->locks == 0
: 現在のOSスレッド(M)が、ランタイム内部のロックを何も保持していないことを確認します。もしロックを保持している場合、そのスレッドはクリティカルセクションにいる可能性があり、そこでスケジューリングを行うとデッドロックを引き起こす可能性があるため、スケジューリングを避けます。dogc
: これがこのコミットで追加された新しい条件です。dogc
は、mallocgc
関数に渡される引数で、このメモリ割り当てがGCをトリガーする可能性があるかどうか、またはGCの対象となるメモリを割り当てるかどうかを示します。例えば、GCの対象とならないスタックメモリの割り当てなどではdogc
がfalse
になることがあります。
&& dogc
の追加の重要性
dogc
が追加されたことにより、runtime·gcwaiting
がtrue
であっても、GCの対象となるメモリ割り当てではない場合には、runtime·gosched()
が呼び出されなくなりました。
ハッシュマップの挿入処理は、必ずしもGCの対象となるメモリ割り当てを伴うわけではありません。例えば、既存のバケットに空きがある場合や、バケットの再ハッシュが必要ない場合などです。以前のコードでは、runtime·gcwaiting
がtrue
であれば、dogc
の値に関わらずruntime·gosched()
が呼び出される可能性がありました。
この変更により、GCが実際にメモリ割り当てを必要とする場合にのみ、ゴルーチンがGCの完了を待つためにスケジューリングされるようになります。これにより、不必要なスケジューリングを避け、特にマルチスレッド環境下でのハッシュマップ挿入時のパフォーマンス低下や、GCとの不必要な相互作用による競合状態のリスクを低減します。
具体的には、ハッシュマップの挿入がGCの対象とならないメモリ割り当てを行う際に、GCが待機状態であってもruntime·gosched()
が呼ばれないことで、ハッシュマップの挿入処理がGCのSTWフェーズに不必要に巻き込まれることを防ぎ、デッドロックやハングアップのリスクをさらに低減します。
コアとなるコードの変更箇所
src/pkg/runtime/malloc.goc
ファイルにおいて、以下の変更が行われました。
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -35,7 +35,7 @@ runtime·mallocgc(uintptr size, uint32 flag, int32 dogc, int32 zeroed)
MSpan *s;
void *v;
- if(runtime·gcwaiting && g != m->g0 && m->locks == 0)
+ if(runtime·gcwaiting && g != m->g0 && m->locks == 0 && dogc)
runtime·gosched();
if(m->mallocing)
runtime·throw("malloc/free - deadlock");
コアとなるコードの解説
変更された行は、runtime·mallocgc
関数内の条件分岐です。
元のコード:
if(runtime·gcwaiting && g != m->g0 && m->locks == 0)
変更後のコード:
if(runtime·gcwaiting && g != m->g0 && m->locks == 0 && dogc)
この変更は、条件式に&& dogc
という新しい条件を追加しています。
runtime·gcwaiting
: ガベージコレクタが現在、アプリケーションゴルーチンが停止するのを待っている状態(STWフェーズの開始など)であることを示します。g != m->g0
: 現在実行中のゴルーチンが、ランタイム内部の特別なゴルーチン(g0
)ではないことを確認します。g0
はGCの対象外であり、GCのSTW中でも実行され続けるため、g0
の場合はスケジューリングを行いません。m->locks == 0
: 現在のOSスレッド(M)が、ランタイム内部のロックを何も保持していないことを確認します。ロックを保持しているクリティカルセクションでスケジューリングを行うとデッドロックを引き起こす可能性があるため、これを避けます。dogc
: この引数は、runtime·mallocgc
が割り当てようとしているメモリがGCの対象となるかどうかを示します。true
であればGCの対象となり、false
であればGCの対象外(例えば、スタックメモリなど)です。
この&& dogc
の追加により、GCが待機状態であっても、GCの対象とならないメモリ割り当ての場合には、runtime·gosched()
(現在のゴルーチンを一時停止してスケジューラに制御を戻す)が呼び出されなくなります。
これにより、ハッシュマップの挿入処理が、GCの対象とならない内部的なメモリ割り当てを行う際に、不必要にGCのSTWフェーズに巻き込まれることを防ぎます。結果として、マルチスレッド環境下でのハッシュマップ挿入時のデッドロックやハングアップのリスクがさらに低減され、ランタイムの安定性とパフォーマンスが向上します。
関連リンク
- Go issue tracker: https://github.com/golang/go/issues (Issue #5074の詳細は、当時のGo issue trackerで確認できますが、古いIssueはアーカイブされている可能性があります。)
- Go source code: https://github.com/golang/go
参考にした情報源リンク
- Goのコミットメッセージと差分情報
- Goランタイムの内部動作に関する一般的な知識(ガベージコレクション、スケジューラ、
map
の実装など) - Goの公式ドキュメントおよび関連する技術ブログ記事(GoのGCや並行処理に関するもの)