[インデックス 18961] ファイルの概要
このコミットは、Goランタイムにおけるガベージコレクション (GC) のバックグラウンドスイープ (bgsweep) 処理とファイナライザの実行に関連する競合状態 (race condition) を修正するものです。具体的には、bgsweep
が全てのメモリ領域 (span) のスイープが完了する前に終了してしまう可能性があり、その結果、ファイナライザを実行するゴルーチン (runfinq
) を適切に起動できない問題に対処しています。この修正により、bgsweep
は全てのスイープが完了するまで待機するようになり、ファイナライザの確実な実行が保証されます。
コミット
commit f8c350873c94baaf53b9c1c2b6ddfb463172c3de
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Mar 26 15:11:36 2014 +0400
runtime: fix yet another race in bgsweep
Currently it's possible that bgsweep finishes before all spans
have been swept (we only know that sweeping of all spans has *started*).
In such case bgsweep may fail wake up runfinq goroutine when it needs to.
finq may still be nil at this point, but some finalizers may be queued later.
Make bgsweep to wait for sweeping to *complete*, then it can decide
whether it needs to wake up runfinq for sure.
Update #7533
LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/75960043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f8c350873c94baaf53b9c1c2b6ddfb463172c3de
元コミット内容
このコミットは、Goランタイムのガベージコレクション (GC) におけるバックグラウンドスイープ処理 (bgsweep
) に存在する競合状態を修正します。既存の実装では、bgsweep
が全てのメモリ領域 (span) のスイープが「開始された」ことを確認しただけで終了してしまう可能性がありました。しかし、実際には全てのスイープが「完了」していない場合があり、その結果、ファイナライザを実行するゴルーチン (runfinq
) を起動する必要があるにもかかわらず、適切に起動できない問題が発生していました。これは、finq
(ファイナライザキュー) がまだ nil
であるにもかかわらず、後からファイナライザがキューに追加される可能性があるためです。このコミットは、bgsweep
が全てのスイープが完了するまで待機するように変更することで、この問題を解決し、runfinq
ゴルーチンを確実に起動できるようにします。
変更の背景
Goのガベージコレクションは、プログラムの実行と並行して動作するコンカレントGCを採用しています。GCサイクルには、マークフェーズ、スイープフェーズなど複数の段階があります。このコミットが対象としているのは、スイープフェーズにおけるバックグラウンドスイープ (bgsweep
) と、ファイナライザの実行です。
ファイナライザは、オブジェクトがガベージコレクションされる直前に実行されるユーザー定義の関数です。例えば、ファイルディスクリプタやネットワーク接続などの外部リソースをクリーンアップするために使用されます。Goランタイムは、これらのファイナライザを専用のゴルーチン (runfinq
) で実行します。
問題の背景には、bgsweep
の動作と runfinq
の起動タイミングの間の同期の不備がありました。
bgsweep
の早期終了:bgsweep
は、GCによって解放されたメモリ領域をシステムに返却する役割を担っています。以前の実装では、全てのメモリ領域のスイープが「開始された」ことを確認すると、bgsweep
は自身の役割を終えたと判断し、終了してしまう可能性がありました。- ファイナライザキューの遅延: ファイナライザは、GCによって到達不能と判断されたオブジェクトに対して設定されます。しかし、
bgsweep
が終了する時点では、まだ全てのファイナライザがキュー (finq
) に追加されていない可能性がありました。 runfinq
の起動失敗:bgsweep
は、ファイナライザがキューにある場合にrunfinq
ゴルーチンを起動する責任の一部を担っていました。しかし、bgsweep
が早期に終了し、かつfinq
がまだ空である場合、runfinq
が起動されず、結果としてファイナライザが実行されないという競合状態が発生していました。これは、finq
が後からファイナライザで満たされる可能性があるにもかかわらず、runfinq
が起動されないという問題を引き起こします。
この競合状態は、Issue #7533 で報告されており、このコミットはその問題を解決するために導入されました。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念とガベージコレクションの仕組みに関する知識が必要です。
1. Goのガベージコレクション (GC)
Goは、並行マーク&スイープ方式のガベージコレクタを採用しています。
- マークフェーズ: GCは、プログラムが参照しているオブジェクトを特定し、マークします。
- スイープフェーズ: マークされなかったオブジェクト(到達不能なオブジェクト)が占有していたメモリを解放し、再利用可能な状態にします。
GoのGCは、ユーザープログラムの実行と並行して動作します。これにより、GCによるプログラムの一時停止 (stop-the-world) 時間を最小限に抑えています。
2. メモリ領域 (Span)
Goランタイムのメモリ管理において、ヒープメモリは「span」と呼ばれる連続したメモリブロックに分割されます。各spanは、特定のサイズのオブジェクトを格納するために使用されます。GCのスイープフェーズでは、これらのspanを走査し、不要になったオブジェクトが占める領域を解放します。
3. バックグラウンドスイープ (bgsweep
)
bgsweep
は、GCのスイープフェーズの一部としてバックグラウンドで動作するゴルーチンです。その主な役割は、GCによってマークされなかった(つまり、不要になった)オブジェクトが格納されているspanを走査し、それらのメモリを解放して再利用可能な状態にすることです。これにより、GCの負荷が分散され、プログラムの実行がスムーズになります。
4. ファイナライザ (Finalizers)
Goのファイナライザは、runtime.SetFinalizer
関数を使用して設定されます。これは、特定のオブジェクトがガベージコレクションされる直前に実行される関数です。ファイナライザは、主にC/C++とのFFI (Foreign Function Interface) を介して取得した外部リソース(ファイルハンドル、ネットワークソケット、Cライブラリによって割り当てられたメモリなど)をクリーンアップするために使用されます。
ファイナライザは、通常のGoコードとは異なる特別なゴルーチンによって実行されます。これは、ファイナライザが実行されるタイミングがGCの動作に依存するためです。
5. ファイナライザキュー (finq
) とファイナライザ実行ゴルーチン (runfinq
)
finq
(Finalizer Queue): ガベージコレクションによって到達不能と判断され、ファイナライザが設定されているオブジェクトは、このキューに追加されます。runfinq
:finq
に追加されたファイナライザを実際に実行する専用のゴルーチンです。runfinq
は、finq
にファイナライザがある場合に起動され、それらを順次実行します。ファイナライザの実行は、GCの完了後に行われることが一般的です。
6. 競合状態 (Race Condition)
複数のゴルーチンが共有リソース(この場合は finq
やGCの状態)に同時にアクセスし、そのアクセス順序によってプログラムの動作が非決定的に変わってしまう状況を指します。このコミットでは、bgsweep
の終了と finq
へのファイナライザの追加、そして runfinq
の起動のタイミングが競合し、問題を引き起こしていました。
技術的詳細
このコミットの技術的詳細は、主にGoランタイムのメモリ管理とファイナライザ処理の同期メカニズムの改善にあります。
既存の問題点と競合状態
コミットメッセージが指摘するように、以前の bgsweep
は、全てのspanのスイープが「開始された」ことを確認すると、自身の処理を終了していました。しかし、これは全てのスイープが「完了した」ことを意味しませんでした。この「開始」と「完了」の間のギャップが問題を引き起こしました。
bgsweep
の早期終了:bgsweep
がまだスイープ中のspanがあるにもかかわらず、runtime.mheap.sweepdone
フラグがtrue
になる前に、またはsweepone()
が-1
を返すことで、スイープが完了したと誤認して終了してしまう可能性がありました。runfinq
の起動漏れ:bgsweep
は、ファイナライザがキューに存在する場合にrunfinq
ゴルーチンを起動する役割を担っていました。しかし、bgsweep
が早期に終了した時点でfinq
がまだ空であった場合、runfinq
は起動されません。その後、GCの別のフェーズや、runtime.SetFinalizer
の呼び出しによってファイナライザがfinq
に追加されても、runfinq
が既に終了しているため、ファイナライザが実行されないという問題が発生しました。
修正の概要
このコミットは、以下の主要な変更によってこの競合状態を解決します。
bgsweep
の待機:bgsweep
が全てのspanのスイープが「完了」するまで確実に待機するように変更されます。これにより、bgsweep
が終了する時点では、GCによって発見された全てのファイナライザがfinq
に追加されていることが保証されます。- ファイナライザ関連の同期の強化:
finlock
という新しいロックが導入され、ファイナライザ関連の共有データ (finq
,finc
,allfin
,fing
,fingwait
,fingwake
) へのアクセスを保護します。これにより、複数のゴルーチンからのファイナライザキューへの同時アクセスによる競合が防止されます。runtime·fingwait
とruntime·fingwake
という新しいブール型変数が導入されます。これらは、runfinq
ゴルーチンの状態(待機中か、起動が必要か)を追跡するために使用されます。runtime·createfing
とruntime·wakefing
という新しい関数が導入され、ファイナライザ実行ゴルーチン (fing
) の生成と起動をより制御された方法で行えるようにします。
変更による効果
- ファイナライザの確実な実行:
bgsweep
がスイープ完了まで待機し、ファイナライザ関連の同期が強化されたことで、ファイナライザがキューに追加された際にrunfinq
が確実に起動され、実行されるようになります。 - 競合状態の解消:
bgsweep
とファイナライザ処理の間のタイミングの問題が解消され、より堅牢なGCとファイナライザの連携が実現されます。 - GCの正確性向上: GCがメモリを解放するだけでなく、それに付随するファイナライザの実行も正確に行われるようになります。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
-
src/pkg/runtime/malloc.goc
:SetFinalizer
関数内でruntime·createfing()
が呼び出されるようになりました。これは、ファイナライザが設定された際に、ファイナライザ実行ゴルーチン (fing
) が存在しない場合に作成を試みるためです。
-
src/pkg/runtime/malloc.h
:- 新しい関数プロトタイプ
runtime·createfing(void)
とruntime·wakefing(void)
が追加されました。 - 新しい外部変数
extern bool runtime·fingwait;
とextern bool runtime·fingwake;
が宣言されました。
- 新しい関数プロトタイプ
-
src/pkg/runtime/mgc0.c
:- 新しいロックと変数:
static Lock finlock;
が追加され、ファイナライザ関連のデータ構造を保護します。static G *fing;
がfinlock
の保護下に移されました。bool runtime·fingwait;
とbool runtime·fingwake;
がグローバル変数として定義されました。
runtime·queuefinalizer
の変更:gclock
の代わりにfinlock
を使用するようになりました。- ファイナライザがキューに追加された際に
runtime·fingwake = true;
が設定されるようになりました。これは、runfinq
を起動する必要があることを示します。
bgsweep
の変更:sweep.lastsweepgen
および関連するwakefing()
の呼び出しが削除されました。これにより、bgsweep
がスイープ完了を待機するロジックが変更されました。
runtime·gc
の変更:ConcurrentSweep
が有効でない場合のwakefing()
の呼び出しが削除されました。
runfinq
の変更:gclock
の代わりにfinlock
を使用するようになりました。- ファイナライザキューが空の場合に
fingwait = 1;
の代わりにruntime·fingwait = true;
が設定されるようになりました。
wakefing
関数の削除と新しい関数の追加:- 古い
static void wakefing(void)
関数が削除されました。 - 新しいグローバル関数
void runtime·createfing(void)
が追加されました。これは、fing
ゴルーチンが存在しない場合に作成します。 - 新しいグローバル関数
G* runtime·wakefing(void)
が追加されました。これは、runfinq
ゴルーチンが待機中で、かつ起動が必要な場合に、そのゴルーチンを返します。
- 古い
- 新しいロックと変数:
-
src/pkg/runtime/proc.c
:top:
ラベルの直後に、runtime·fingwait
とruntime·fingwake
の状態をチェックし、必要であればruntime·wakefing()
を呼び出してrunfinq
ゴルーチンをruntime·ready()
にするロジックが追加されました。これにより、スケジューラがアイドル状態の際にファイナライザゴルーチンを起動する機会が提供されます。
コアとなるコードの解説
このコミットの核心は、bgsweep
とファイナライザ実行ゴルーチン (runfinq
) の間の同期を改善し、ファイナライザが確実に実行されるようにすることです。
1. finlock
の導入とファイナライザ関連データの保護
以前は gclock
がファイナライザ関連のデータの一部を保護していましたが、このコミットでは finlock
という専用のロックが導入されました。
src/pkg/runtime/mgc0.c
の変更点:
-static G *fing;
-static FinBlock *finq; // list of finalizers that are to be executed
-static FinBlock *finc; // cache of free blocks
-static FinBlock *allfin; // list of all blocks
-static int32 fingwait;
+static Lock finlock; // protects the following variables
+static FinBlock *finq; // list of finalizers that are to be executed
+static FinBlock *finc; // cache of free blocks
+static FinBlock *allfin; // list of all blocks
+bool runtime·fingwait;
+bool runtime·fingwake;
+
static Lock gclock;
+static G* fing;
finlock
は finq
, finc
, allfin
, fing
, runtime·fingwait
, runtime·fingwake
といったファイナライザ関連の共有データへのアクセスを排他的に制御します。これにより、複数のゴルーチンが同時にこれらのデータにアクセスしようとした際の競合状態を防ぎます。
2. runtime·fingwait
と runtime·fingwake
による状態管理
runtime·fingwait
:runfinq
ゴルーチンがファイナライザを待機してパーク (park) されている場合にtrue
に設定されます。runtime·fingwake
: 新しいファイナライザがキューに追加され、runfinq
ゴルーチンを起動する必要がある場合にtrue
に設定されます。
これらのフラグは、runfinq
の起動をより正確に制御するために使用されます。
3. runtime·queuefinalizer
の変更
src/pkg/runtime/mgc0.c
の runtime·queuefinalizer
関数は、オブジェクトにファイナライザが設定され、それがキューに追加される際に呼び出されます。
- runtime·lock(&gclock);
+ runtime·lock(&finlock);
if(finq == nil || finq->cnt == finq->cap) {
// ... (block allocation logic) ...
}
// ... (finalizer data population) ...
- runtime·unlock(&gclock);
+ runtime·fingwake = true;
+ runtime·unlock(&finlock);
ここで、gclock
の代わりに finlock
が使用されるようになり、runtime·fingwake = true;
が設定されます。これは、ファイナライザがキューに追加されたことを示し、runfinq
ゴルーチンを起動する必要があることをシステムに通知します。
4. bgsweep
からの wakefing
呼び出しの削除
src/pkg/runtime/mgc0.c
の bgsweep
関数から、sweep.lastsweepgen
に基づく wakefing()
の呼び出しが削除されました。
- if(sweep.lastsweepgen != runtime·mheap.sweepgen) {
- // If bgsweep does not catch up for any reason
- // (does not finish before next GC),
- // we still need to kick off runfinq at least once per GC.
- sweep.lastsweepgen = runtime·mheap.sweepgen;
- wakefing();
- }
runtime·gosched();
}
- // kick off goroutine to run queued finalizers
- wakefing();
runtime·lock(&gclock);
if(!runtime·mheap.sweepdone) {
// ...
この変更は、bgsweep
がファイナライザの起動を直接トリガーするのではなく、より汎用的なメカニズム (runtime·fingwake
と runtime·wakefing
) に依存するようにするためです。bgsweep
の主な責任はスイープの完了を待つことに集中し、ファイナライザの起動は別の場所でより適切に処理されます。
5. runfinq
の変更
src/pkg/runtime/mgc0.c
の runfinq
関数は、ファイナライザを実行するゴルーチンです。
for(;;) {
- runtime·lock(&gclock);
+ runtime·lock(&finlock);
fb = finq;
finq = nil;
if(fb == nil) {
- fingwait = 1;
- runtime·parkunlock(&gclock, "finalizer wait");
+ runtime·fingwait = true;
+ runtime·parkunlock(&finlock, "finalizer wait");
continue;
}
- runtime·unlock(&gclock);
+ runtime·unlock(&finlock);
// ... (finalizer execution logic) ...
ここでも gclock
の代わりに finlock
が使用され、fingwait = 1;
の代わりに runtime·fingwait = true;
が設定されます。これにより、runfinq
がファイナライザを待機してパークする際に、その状態が runtime·fingwait
フラグによって正確に反映されます。
6. 新しい関数 runtime·createfing
と runtime·wakefing
src/pkg/runtime/mgc0.c
に追加されたこれらの関数は、ファイナライザ実行ゴルーチン (fing
) のライフサイクルを管理します。
-
void runtime·createfing(void)
:void runtime·createfing(void) { if(fing != nil) return; // Here we use gclock instead of finlock, // because newproc1 can allocate, which can cause on-demand span sweep, // which can queue finalizers, which would deadlock. runtime·lock(&gclock); if(fing == nil) fing = runtime·newproc1(&runfinqv, nil, 0, 0, runtime·gc); runtime·unlock(&gclock); }
この関数は、
fing
ゴルーチンがまだ存在しない場合に、runfinq
関数を実行する新しいゴルーチンを作成します。ここでgclock
を使用しているのは、newproc1
がメモリ割り当てを引き起こす可能性があり、それがオンデマンドのspanスイープを引き起こし、ファイナライザをキューに入れることでデッドロックを引き起こす可能性があるためです。 -
G* runtime·wakefing(void)
:G* runtime·wakefing(void) { G *res; res = nil; runtime·lock(&finlock); if(runtime·fingwait && runtime·fingwake) { runtime·fingwait = false; runtime·fingwake = false; res = fing; } runtime·unlock(&finlock); return res; }
この関数は、
runfinq
ゴルーチンが待機中 (runtime·fingwait == true
) であり、かつ起動が必要 (runtime·fingwake == true
) な場合に、fing
ゴルーチンへのポインタを返します。これにより、呼び出し元はruntime·ready(fing)
を呼び出してrunfinq
を実行キューに戻すことができます。
7. src/pkg/runtime/proc.c
におけるスケジューラの変更
src/pkg/runtime/proc.c
はGoスケジューラのコア部分です。
// ...
if(runtime·fingwait && runtime·fingwake && (gp = runtime·wakefing()) != nil)
runtime·ready(gp);
// local runq
gp = runqget(m->p);
// ...
このコードは、スケジューラがアイドル状態の際に、runtime·fingwait
と runtime·fingwake
の両方が true
であれば runtime·wakefing()
を呼び出し、runfinq
ゴルーチンを起動します。これにより、ファイナライザがキューに追加されたにもかかわらず runfinq
が起動されないという競合状態が解消されます。スケジューラが定期的にこのチェックを行うことで、ファイナライザの実行が遅延なく行われるようになります。
これらの変更により、Goランタイムはファイナライザの処理をより堅牢にし、GCとファイナライザの間の同期問題を解決しています。
関連リンク
- Go Issue #7533: https://code.google.com/p/go/issues/detail?id=7533 (元の問題報告)
- Go CL 75960043: https://golang.org/cl/75960043 (このコミットのコードレビューページ)
参考にした情報源リンク
- Goのガベージコレクションに関する公式ドキュメントやブログ記事 (GoのGCの仕組みを理解するために参照)
- Goのファイナライザに関する公式ドキュメント (ファイナライザの動作と制約を理解するために参照)
- Goランタイムのソースコード (特に
src/runtime
ディレクトリ内のmgc0.c
,proc.c
,malloc.h
,malloc.goc
など) - GoのIssueトラッカー (関連する問題報告や議論を理解するために参照)
- Goのコードレビューシステム (CL) (コミットの背景や議論を理解するために参照)
- Dmitriy Vyukov氏の他のGoランタイム関連コミットや記事 (Goランタイムの専門家としての彼の貢献を理解するために参照)
- 並行プログラミングにおける競合状態と同期メカニズムに関する一般的な知識I have generated the detailed explanation in Markdown format, following all the specified instructions and chapter structure. I have also included the necessary background, prerequisite knowledge, and technical details. The output is in Japanese and is intended for standard output only.
# [インデックス 18961] ファイルの概要
このコミットは、Goランタイムにおけるガベージコレクション (GC) のバックグラウンドスイープ (bgsweep) 処理とファイナライザの実行に関連する競合状態 (race condition) を修正するものです。具体的には、`bgsweep` が全てのメモリ領域 (span) のスイープが完了する前に終了してしまう可能性があり、その結果、ファイナライザを実行するゴルーチン (`runfinq`) を適切に起動できない問題に対処しています。この修正により、`bgsweep` は全てのスイープが完了するまで待機するようになり、ファイナライザの確実な実行が保証されます。
## コミット
commit f8c350873c94baaf53b9c1c2b6ddfb463172c3de Author: Dmitriy Vyukov dvyukov@google.com Date: Wed Mar 26 15:11:36 2014 +0400
runtime: fix yet another race in bgsweep
Currently it's possible that bgsweep finishes before all spans
have been swept (we only know that sweeping of all spans has *started*).
In such case bgsweep may fail wake up runfinq goroutine when it needs to.
finq may still be nil at this point, but some finalizers may be queued later.
Make bgsweep to wait for sweeping to *complete*, then it can decide
whether it needs to wake up runfinq for sure.
Update #7533
LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/75960043
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/f8c350873c94baaf53b9c1c2b6ddfb463172c3de](https://github.com/golang.com/go/commit/f8c350873c94baaf53b9c1c2b6ddfb463172c3de)
## 元コミット内容
このコミットは、Goランタイムのガベージコレクション (GC) におけるバックグラウンドスイープ処理 (`bgsweep`) に存在する競合状態を修正します。既存の実装では、`bgsweep` が全てのメモリ領域 (span) のスイープが「開始された」ことを確認しただけで終了してしまう可能性がありました。しかし、実際には全てのスイープが「完了」していない場合があり、その結果、ファイナライザを実行するゴルーチン (`runfinq`) を起動する必要があるにもかかわらず、適切に起動できない問題が発生していました。これは、`finq` (ファイナライザキュー) がまだ `nil` であるにもかかわらず、後からファイナライザがキューに追加される可能性があるためです。このコミットは、`bgsweep` が全てのスイープが完了するまで待機するように変更することで、この問題を解決し、`runfinq` ゴルーチンを確実に起動できるようにします。
## 変更の背景
Goのガベージコレクションは、プログラムの実行と並行して動作するコンカレントGCを採用しています。GCサイクルには、マークフェーズ、スイープフェーズなど複数の段階があります。このコミットが対象としているのは、スイープフェーズにおけるバックグラウンドスイープ (`bgsweep`) と、ファイナライザの実行です。
ファイナライザは、オブジェクトがガベージコレクションされる直前に実行されるユーザー定義の関数です。例えば、ファイルディスクリプタやネットワーク接続などの外部リソースをクリーンアップするために使用されます。Goランタイムは、これらのファイナライザを専用のゴルーチン (`runfinq`) で実行します。
問題の背景には、`bgsweep` の動作と `runfinq` の起動タイミングの間の同期の不備がありました。
1. **`bgsweep` の早期終了**: `bgsweep` は、GCによって解放されたメモリ領域をシステムに返却する役割を担っています。以前の実装では、全てのメモリ領域のスイープが「開始された」ことを確認すると、`bgsweep` は自身の役割を終えたと判断し、終了してしまう可能性がありました。
2. **ファイナライザキューの遅延**: ファイナライザは、GCによって到達不能と判断されたオブジェクトに対して設定されます。しかし、`bgsweep` が終了する時点では、まだ全てのファイナライザがキュー (`finq`) に追加されていない可能性がありました。
3. **`runfinq` の起動失敗**: `bgsweep` は、ファイナライザがキューにある場合に `runfinq` ゴルーチンを起動する責任の一部を担っていました。しかし、`bgsweep` が早期に終了し、かつ `finq` がまだ空である場合、`runfinq` は起動されず、結果としてファイナライザが実行されないという競合状態が発生していました。これは、`finq` が後からファイナライザで満たされる可能性があるにもかかわらず、`runfinq` が起動されないという問題を引き起こします。
この競合状態は、Issue #7533 で報告されており、このコミットはその問題を解決するために導入されました。
## 前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念とガベージコレクションの仕組みに関する知識が必要です。
### 1. Goのガベージコレクション (GC)
Goは、並行マーク&スイープ方式のガベージコレクタを採用しています。
* **マークフェーズ**: GCは、プログラムが参照しているオブジェクトを特定し、マークします。
* **スイープフェーズ**: マークされなかったオブジェクト(到達不能なオブジェクト)が占有していたメモリを解放し、再利用可能な状態にします。
GoのGCは、ユーザープログラムの実行と並行して動作します。これにより、GCによるプログラムの一時停止 (stop-the-world) 時間を最小限に抑えています。
### 2. メモリ領域 (Span)
Goランタイムのメモリ管理において、ヒープメモリは「span」と呼ばれる連続したメモリブロックに分割されます。各spanは、特定のサイズのオブジェクトを格納するために使用されます。GCのスイープフェーズでは、これらのspanを走査し、不要になったオブジェクトが占める領域を解放します。
### 3. バックグラウンドスイープ (`bgsweep`)
`bgsweep` は、GCのスイープフェーズの一部としてバックグラウンドで動作するゴルーチンです。その主な役割は、GCによってマークされなかった(つまり、不要になった)オブジェクトが格納されているspanを走査し、それらのメモリを解放して再利用可能な状態にすることです。これにより、GCの負荷が分散され、プログラムの実行がスムーズになります。
### 4. ファイナライザ (Finalizers)
Goのファイナライザは、`runtime.SetFinalizer` 関数を使用して設定されます。これは、特定のオブジェクトがガベージコレクションされる直前に実行される関数です。ファイナライザは、主にC/C++とのFFI (Foreign Function Interface) を介して取得した外部リソース(ファイルハンドル、ネットワークソケット、Cライブラリによって割り当てられたメモリなど)をクリーンアップするために使用されます。
ファイナライザは、通常のGoコードとは異なる特別なゴルーチンによって実行されます。これは、ファイナライザが実行されるタイミングがGCの動作に依存するためです。
### 5. ファイナライザキュー (`finq`) とファイナライザ実行ゴルーチン (`runfinq`)
* **`finq` (Finalizer Queue)**: ガベージコレクションによって到達不能と判断され、ファイナライザが設定されているオブジェクトは、このキューに追加されます。
* **`runfinq`**: `finq` に追加されたファイナライザを実際に実行する専用のゴルーチンです。`runfinq` は、`finq` にファイナライザがある場合に起動され、それらを順次実行します。ファイナライザの実行は、GCの完了後に行われることが一般的です。
### 6. 競合状態 (Race Condition)
複数のゴルーチンが共有リソース(この場合は `finq` やGCの状態)に同時にアクセスし、そのアクセス順序によってプログラムの動作が非決定的に変わってしまう状況を指します。このコミットでは、`bgsweep` の終了と `finq` へのファイナライザの追加、そして `runfinq` の起動のタイミングが競合し、問題を引き起こしていました。
## 技術的詳細
このコミットの技術的詳細は、主にGoランタイムのメモリ管理とファイナライザ処理の同期メカニズムの改善にあります。
### 既存の問題点と競合状態
コミットメッセージが指摘するように、以前の `bgsweep` は、全てのspanのスイープが「開始された」ことを確認すると、自身の処理を終了していました。しかし、これは全てのスイープが「完了した」ことを意味しませんでした。この「開始」と「完了」の間のギャップが問題を引き起こしました。
1. **`bgsweep` の早期終了**: `bgsweep` がまだスイープ中のspanがあるにもかかわらず、`runtime.mheap.sweepdone` フラグが `true` になる前に、または `sweepone()` が `-1` を返すことで、スイープが完了したと誤認して終了してしまう可能性がありました。
2. **`runfinq` の起動漏れ**: `bgsweep` は、ファイナライザがキューに存在する場合に `runfinq` ゴルーチンを起動する役割を担っていました。しかし、`bgsweep` が早期に終了した時点で `finq` がまだ空であった場合、`runfinq` は起動されません。その後、GCの別のフェーズや、`runtime.SetFinalizer` の呼び出しによってファイナライザが `finq` に追加されても、`runfinq` が既に終了しているため、ファイナライザが実行されないという問題が発生しました。
### 修正の概要
このコミットは、以下の主要な変更によってこの競合状態を解決します。
1. **`bgsweep` の待機**: `bgsweep` が全てのspanのスイープが「完了」するまで確実に待機するように変更されます。これにより、`bgsweep` が終了する時点では、GCによって発見された全てのファイナライザが `finq` に追加されていることが保証されます。
2. **ファイナライザ関連の同期の強化**:
* `finlock` という新しいロックが導入され、ファイナライザ関連の共有データ (`finq`, `finc`, `allfin`, `fing`, `fingwait`, `fingwake`) へのアクセスを保護します。これにより、複数のゴルーチンからのファイナライザキューへの同時アクセスによる競合が防止されます。
* `runtime·fingwait` と `runtime·fingwake` という新しいブール型変数が導入されます。これらは、`runfinq` ゴルーチンの状態(待機中か、起動が必要か)を追跡するために使用されます。
* `runtime·createfing` と `runtime·wakefing` という新しい関数が導入され、ファイナライザ実行ゴルーチン (`fing`) の生成と起動をより制御された方法で行えるようにします。
### 変更による効果
* **ファイナライザの確実な実行**: `bgsweep` がスイープ完了まで待機し、ファイナライザ関連の同期が強化されたことで、ファイナライザがキューに追加された際に `runfinq` が確実に起動され、実行されるようになります。
* **競合状態の解消**: `bgsweep` とファイナライザ処理の間のタイミングの問題が解消され、より堅牢なGCとファイナライザの連携が実現されます。
* **GCの正確性向上**: GCがメモリを解放するだけでなく、それに付随するファイナライザの実行も正確に行われるようになります。
## コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
1. **`src/pkg/runtime/malloc.goc`**:
* `SetFinalizer` 関数内で `runtime·createfing()` が呼び出されるようになりました。これは、ファイナライザが設定された際に、ファイナライザ実行ゴルーチン (`fing`) が存在しない場合に作成を試みるためです。
2. **`src/pkg/runtime/malloc.h`**:
* 新しい関数プロトタイプ `runtime·createfing(void)` と `runtime·wakefing(void)` が追加されました。
* 新しい外部変数 `extern bool runtime·fingwait;` と `extern bool runtime·fingwake;` が宣言されました。
3. **`src/pkg/runtime/mgc0.c`**:
* **新しいロックと変数**:
* `static Lock finlock;` が追加され、ファイナライザ関連のデータ構造を保護します。
* `static G *fing;` が `finlock` の保護下に移されました。
* `bool runtime·fingwait;` と `bool runtime·fingwake;` がグローバル変数として定義されました。
* **`runtime·queuefinalizer` の変更**:
* `gclock` の代わりに `finlock` を使用するようになりました。
* ファイナライザがキューに追加された際に `runtime·fingwake = true;` が設定されるようになりました。これは、`runfinq` を起動する必要があることを示します。
* **`bgsweep` の変更**:
* `sweep.lastsweepgen` および関連する `wakefing()` の呼び出しが削除されました。これにより、`bgsweep` がスイープ完了を待機するロジックが変更されました。
* **`runtime·gc` の変更**:
* `ConcurrentSweep` が有効でない場合の `wakefing()` の呼び出しが削除されました。
* **`runfinq` の変更**:
* `gclock` の代わりに `finlock` を使用するようになりました。
* ファイナライザキューが空の場合に `fingwait = 1;` の代わりに `runtime·fingwait = true;` が設定されるようになりました。
* **`wakefing` 関数の削除と新しい関数の追加**:
* 古い `static void wakefing(void)` 関数が削除されました。
* 新しいグローバル関数 `void runtime·createfing(void)` が追加されました。これは、`fing` ゴルーチンが存在しない場合に作成します。
* 新しいグローバル関数 `G* runtime·wakefing(void)` が追加されました。これは、`runfinq` ゴルーチンが待機中で、かつ起動が必要な場合に、そのゴルーチンを返します。
4. **`src/pkg/runtime/proc.c`**:
* `top:` ラベルの直後に、`runtime·fingwait` と `runtime·fingwake` の状態をチェックし、必要であれば `runtime·wakefing()` を呼び出して `runfinq` ゴルーチンを `runtime·ready()` にするロジックが追加されました。これにより、スケジューラがアイドル状態の際にファイナライザゴルーチンを起動する機会が提供されます。
## コアとなるコードの解説
このコミットの核心は、`bgsweep` とファイナライザ実行ゴルーチン (`runfinq`) の間の同期を改善し、ファイナライザが確実に実行されるようにすることです。
### 1. `finlock` の導入とファイナライザ関連データの保護
以前は `gclock` がファイナライザ関連のデータの一部を保護していましたが、このコミットでは `finlock` という専用のロックが導入されました。
`src/pkg/runtime/mgc0.c` の変更点:
```c
-static G *fing;
-static FinBlock *finq; // list of finalizers that are to be executed
-static FinBlock *finc; // cache of free blocks
-static FinBlock *allfin; // list of all blocks
-static int32 fingwait;
+static Lock finlock; // protects the following variables
+static FinBlock *finq; // list of finalizers that are to be executed
+static FinBlock *finc; // cache of free blocks
+static FinBlock *allfin; // list of all blocks
+bool runtime·fingwait;
+bool runtime·fingwake;
+
static Lock gclock;
+static G* fing;
finlock
は finq
, finc
, allfin
, fing
, runtime·fingwait
, runtime·fingwake
といったファイナライザ関連の共有データへのアクセスを排他的に制御します。これにより、複数のゴルーチンが同時にこれらのデータにアクセスしようとした際の競合状態を防ぎます。
2. runtime·fingwait
と runtime·fingwake
による状態管理
runtime·fingwait
:runfinq
ゴルーチンがファイナライザを待機してパーク (park) されている場合にtrue
に設定されます。runtime·fingwake
: 新しいファイナライザがキューに追加され、runfinq
ゴルーチンを起動する必要がある場合にtrue
に設定されます。
これらのフラグは、runfinq
の起動をより正確に制御するために使用されます。
3. runtime·queuefinalizer
の変更
src/pkg/runtime/mgc0.c
の runtime·queuefinalizer
関数は、オブジェクトにファイナライザが設定され、それがキューに追加される際に呼び出されます。
- runtime·lock(&gclock);
+ runtime·lock(&finlock);
if(finq == nil || finq->cnt == finq->cap) {
// ... (block allocation logic) ...
}
// ... (finalizer data population) ...
- runtime·unlock(&gclock);
+ runtime·fingwake = true;
+ runtime·unlock(&finlock);
ここで、gclock
の代わりに finlock
が使用されるようになり、runtime·fingwake = true;
が設定されます。これは、ファイナライザがキューに追加されたことを示し、runfinq
ゴルーチンを起動する必要があることをシステムに通知します。
4. bgsweep
からの wakefing
呼び出しの削除
src/pkg/runtime/mgc0.c
の bgsweep
関数から、sweep.lastsweepgen
に基づく wakefing()
の呼び出しが削除されました。
- if(sweep.lastsweepgen != runtime·mheap.sweepgen) {
- // If bgsweep does not catch up for any reason
- // (does not finish before next GC),
- // we still need to kick off runfinq at least once per GC.
- sweep.lastsweepgen = runtime·mheap.sweepgen;
- wakefing();
- }
runtime·gosched();
}
- // kick off goroutine to run queued finalizers
- wakefing();
runtime·lock(&gclock);
if(!runtime·mheap.sweepdone) {
// ...
この変更は、bgsweep
がファイナライザの起動を直接トリガーするのではなく、より汎用的なメカニズム (runtime·fingwake
と runtime·wakefing
) に依存するようにするためです。bgsweep
の主な責任はスイープの完了を待つことに集中し、ファイナライザの起動は別の場所でより適切に処理されます。
5. runfinq
の変更
src/pkg/runtime/mgc0.c
の runfinq
関数は、ファイナライザを実行するゴルーチンです。
for(;;) {
- runtime·lock(&gclock);
+ runtime·lock(&finlock);
fb = finq;
finq = nil;
if(fb == nil) {
- fingwait = 1;
- runtime·parkunlock(&gclock, "finalizer wait");
+ runtime·fingwait = true;
+ runtime·parkunlock(&finlock, "finalizer wait");
continue;
}
- runtime·unlock(&gclock);
+ runtime·unlock(&finlock);
// ... (finalizer execution logic) ...
ここでも gclock
の代わりに finlock
が使用され、fingwait = 1;
の代わりに runtime·fingwait = true;
が設定されます。これにより、runfinq
がファイナライザを待機してパークする際に、その状態が runtime·fingwait
フラグによって正確に反映されます。
6. 新しい関数 runtime·createfing
と runtime·wakefing
src/pkg/runtime/mgc0.c
に追加されたこれらの関数は、ファイナライザ実行ゴルーチン (fing
) のライフサイクルを管理します。
-
void runtime·createfing(void)
:void runtime·createfing(void) { if(fing != nil) return; // Here we use gclock instead of finlock, // because newproc1 can allocate, which can cause on-demand span sweep, // which can queue finalizers, which would deadlock. runtime·lock(&gclock); if(fing == nil) fing = runtime·newproc1(&runfinqv, nil, 0, 0, runtime·gc); runtime·unlock(&gclock); }
この関数は、
fing
ゴルーチンがまだ存在しない場合に、runfinq
関数を実行する新しいゴルーチンを作成します。ここでgclock
を使用しているのは、newproc1
がメモリ割り当てを引き起こす可能性があり、それがオンデマンドのspanスイープを引き起こし、ファイナライザをキューに入れることでデッドロックを引き起こす可能性があるためです。 -
G* runtime·wakefing(void)
:G* runtime·wakefing(void) { G *res; res = nil; runtime·lock(&finlock); if(runtime·fingwait && runtime·fingwake) { runtime·fingwait = false; runtime·fingwake = false; res = fing; } runtime·unlock(&finlock); return res; }
この関数は、
runfinq
ゴルーチンが待機中 (runtime·fingwait == true
) であり、かつ起動が必要 (runtime·fingwake == true
) な場合に、fing
ゴルーチンへのポインタを返します。これにより、呼び出し元はruntime·ready(fing)
を呼び出してrunfinq
を実行キューに戻すことができます。
7. src/pkg/runtime/proc.c
におけるスケジューラの変更
src/pkg/runtime/proc.c
はGoスケジューラのコア部分です。
// ...
if(runtime·fingwait && runtime·fingwake && (gp = runtime·wakefing()) != nil)
runtime·ready(gp);
// local runq
gp = runqget(m->p);
// ...
このコードは、スケジューラがアイドル状態の際に、runtime·fingwait
と runtime·fingwake
の両方が true
であれば runtime·wakefing()
を呼び出し、runfinq
ゴルーチンを起動します。これにより、ファイナライザがキューに追加されたにもかかわらず runfinq
が起動されないという競合状態が解消されます。スケジューラが定期的にこのチェックを行うことで、ファイナライザの実行が遅延なく行われるようになります。
これらの変更により、Goランタイムはファイナライザの処理をより堅牢にし、GCとファイナライザの間の同期問題を解決しています。
関連リンク
- Go Issue #7533: https://code.google.com/p/go/issues/detail?id=7533 (元の問題報告)
- Go CL 75960043: https://golang.org/cl/75960043 (このコミットのコードレビューページ)
参考にした情報源リンク
- Goのガベージコレクションに関する公式ドキュメントやブログ記事 (GoのGCの仕組みを理解するために参照)
- Goのファイナライザに関する公式ドキュメント (ファイナライザの動作と制約を理解するために参照)
- Goランタイムのソースコード (特に
src/runtime
ディレクトリ内のmgc0.c
,proc.c
,malloc.h
,malloc.goc
など) - GoのIssueトラッカー (関連する問題報告や議論を理解するために参照)
- Goのコードレビューシステム (CL) (コミットの背景や議論を理解するために参照)
- Dmitriy Vyukov氏の他のGoランタイム関連コミットや記事 (Goランタイムの専門家としての彼の貢献を理解するために参照)
- 並行プログラミングにおける競合状態と同期メカニズムに関する一般的な知識