[インデックス 18477] ファイルの概要
このコミットは、Goランタイムにおけるガベージコレクション(GC)の非並行スイープ処理において、ファイナライザが実行されない問題を修正するものです。これは、並行スイープの導入とその後の問題により、一時的に非並行スイープに戻された状況下での応急処置として適用されました。
コミット
commit 73a304356bd1edfac204c639859a01643a3f8955
Author: Russ Cox <rsc@golang.org>
Date: Wed Feb 12 15:54:21 2014 -0500
runtime: fix non-concurrent sweep
State of the world:
CL 46430043 introduced a new concurrent sweep but is broken.
CL 62360043 made the new sweep non-concurrent
to try to fix the world while we understand what's wrong with
the concurrent version.
This CL fixes the non-concurrent form to run finalizers.
This CL is just a band-aid to get the build green again.
Dmitriy is working on understanding and then fixing what's
wrong with the concurrent sweep.
TBR=dvyukov
CC=golang-codereviews
https://golang.org/cl/62370043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/73a304356bd1edfac204c639859a01643a3f8955
元コミット内容
Goランタイムにおいて、非並行スイープがファイナライザを実行しない問題を修正します。これは、並行スイープの導入とその後の不具合により、一時的に非並行スイープに戻された状況下での暫定的な修正です。並行スイープの問題については、Dmitriyが調査・修正中です。
変更の背景
このコミットは、Goのガベージコレクション(GC)における重要な過渡期に位置しています。当時のGo(特にGo 1.3リリース前後)では、GCのパフォーマンス改善、特に「Stop-The-World (STW)」ポーズ時間の削減が大きな課題でした。その解決策の一つとして、GCのスイープフェーズをアプリケーションの実行と並行して行う「並行スイープ」の導入が試みられていました。
コミットメッセージによると、以下の経緯がありました。
CL 46430043
(Change List) によって新しい並行スイープが導入されました。- しかし、この並行スイープには不具合があり、ビルドが失敗するなどの問題が発生していました。
- 問題を一時的に回避し、ビルドを再び正常な状態に戻すため、
CL 62360043
によって新しいスイープは非並行モードに切り替えられました。 - このコミット (
CL 62370043
) は、非並行に戻されたスイープがファイナライザを正しく実行しないという新たな問題に対応するためのものです。これはあくまで「バンドエイド(応急処置)」であり、根本的な並行スイープの問題はDmitriy氏が引き続き調査・修正にあたっていました。
この修正は、並行GCへの移行期における安定性確保のための重要なステップでした。
前提知識の解説
Goのガベージコレクション (GC)
Goのガベージコレクションは、主に「マーク・アンド・スイープ」アルゴリズムに基づいています。
- マークフェーズ (Mark Phase): GCが実行されると、まずプログラムの実行が一時停止(Stop-The-World: STW)され、到達可能なオブジェクト(まだ使用されているオブジェクト)がマークされます。
- スイープフェーズ (Sweep Phase): マークされなかったオブジェクト(到達不可能で、もはや使用されていないオブジェクト)がメモリから解放されます。
Go 1.3(2014年リリース)では、スイープフェーズの一部が並行化されました。これは、スイープ処理の一部をアプリケーションの実行と並行して行うことで、STWポーズ時間を短縮することを目的としていました。しかし、完全に並行なマーク・アンド・スイープGCが導入され、STWポーズが劇的に削減されたのはGo 1.5(2015年リリース)以降です。
ファイナライザ (Finalizers)
Goでは、runtime.SetFinalizer
関数を使ってオブジェクトにファイナライザを設定できます。ファイナライザは、そのオブジェクトがガベージコレクションによってメモリから解放される直前に実行される関数です。主に、ファイルハンドルやネットワーク接続などの非メモリリソースをクリーンアップするために使用されます。
ファイナライザはGCによってキューに入れられ、GCサイクルが完了した後に専用のゴルーチン(ファイナライザゴルーチン)によって実行されます。GCはファイナライザの実行を待たずに自身のサイクルを完了します。
並行スイープと非並行スイープ
- 並行スイープ (Concurrent Sweep): GCのスイープ処理が、アプリケーションの通常の実行と並行して行われます。これにより、アプリケーションの応答性が向上し、STWポーズ時間が短縮されます。
- 非並行スイープ (Non-Concurrent Sweep): GCのスイープ処理が、アプリケーションの実行を完全に停止させて行われます。これはSTWポーズ時間を長くする原因となりますが、実装は比較的単純です。
このコミットの時点では、並行スイープの実装に問題があったため、一時的に非並行スイープに戻されていました。このコミットは、その非並行スイープがファイナライザを正しく処理しないというバグを修正するものです。
技術的詳細
このコミットの技術的詳細は、Goランタイムのガベージコレクションの内部動作、特にスイープフェーズとファイナライザの連携に焦点を当てています。
変更は src/pkg/runtime/mgc0.c
ファイルに対して行われています。このファイルは、GoランタイムのGCのコアロジックをC言語で記述したものです。
主要な変更点は以下の2つです。
-
ConcurrentSweep
フラグの導入と利用: 以前は、並行スイープを一時的に無効にするためにif(false)
というハードコードされた条件が使われていました。これは、デバッグや一時的な回避策としては機能しますが、コードの意図を不明瞭にし、将来的な変更を困難にします。 このコミットでは、enum
にConcurrentSweep = 0,
という定数を追加し、if(false)
の代わりにif(ConcurrentSweep)
を使用するように変更しています。ConcurrentSweep
が0
であるため、この条件は常にfalse
となり、並行スイープは無効化されたままになります。これにより、コードの可読性と保守性が向上し、将来的に並行スイープを有効にする際にこのフラグを変更するだけで済むようになります。 -
非並行スイープ後のファイナライザ実行ロジックの追加: GCの
runtime·gc
関数内で、GCが完了した後にファイナライザを処理するロジックが追加されました。if(!ConcurrentSweep)
: このブロックは、並行スイープが有効でない(つまり、非並行スイープが実行されている)場合にのみ実行されます。if(finq != nil)
:finq
はファイナライザのキュー(リスト)を指すポインタです。キューにファイナライザが登録されている場合にのみ、以下の処理に進みます。runtime·lock(&gclock)
: GC関連のグローバルロックを取得し、競合状態を防ぎます。if(fing == nil)
:fing
はファイナライザを実行するゴルーチン(finalizer goroutine)を指すポインタです。もしファイナライザゴルーチンがまだ存在しない場合、runtime·newproc1
を使って新しいファイナライザゴルーチン (runfinqv
) を作成し、起動します。else if(fingwait)
: ファイナライザゴルーチンが既に存在し、かつ待機状態 (fingwait
) の場合、fingwait
フラグをクリアし、runtime·ready(fing)
を使ってそのゴルーチンを再開可能な状態にします。runtime·unlock(&gclock)
: ロックを解放します。runtime·gosched()
: 現在のゴルーチンを一時停止し、他のゴルーチン(特にファイナライザゴルーチン)に実行機会を与えます。これにより、キューに入れられたファイナライザがすぐに実行される可能性が高まります。
この修正により、並行スイープが一時的に無効化されている間でも、ファイナライザが正しく起動され、登録されたクリーンアップ処理が実行されるようになりました。これは、メモリリークやリソースリークを防ぐ上で非常に重要です。
コアとなるコードの変更箇所
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -66,6 +66,7 @@ enum {
CollectStats = 0,
ScanStackByFrames = 1,
IgnorePreciseGC = 0,
++ ConcurrentSweep = 0,
// Four bits per word (see #defines below).
wordsPerBitmapWord = sizeof(void*)*8/4,
@@ -2237,6 +2238,23 @@ runtime·gc(int32 force)
runtime·semrelease(&runtime·worldsema);
runtime·starttheworld();
m->locks--;
++
++ // now that gc is done, kick off finalizer thread if needed
++ if(!ConcurrentSweep) {
++ if(finq != nil) {
++ runtime·lock(&gclock);
++ // kick off or wake up goroutine to run queued finalizers
++ if(fing == nil)
++ fing = runtime·newproc1(&runfinqv, nil, 0, 0, runtime·gc);
++ else if(fingwait) {
++ fingwait = 0;
++ runtime·ready(fing);
++ }
++ runtime·unlock(&gclock);
++ }
++ // give the queued finalizers, if any, a chance to run
++ runtime·gosched();
++ }
}
static void
@@ -2384,7 +2402,7 @@ gc(struct gc_args *args)
sweep.spanidx = 0;
// Temporary disable concurrent sweep, because we see failures on builders.
-- if(false) {
++ if(ConcurrentSweep) {
runtime·lock(&gclock);
if(sweep.g == nil)
sweep.g = runtime·newproc1(&bgsweepv, nil, 0, 0, runtime·gc);
コアとなるコードの解説
1. enum
への ConcurrentSweep
定数の追加
@@ -66,6 +66,7 @@ enum {
CollectStats = 0,
ScanStackByFrames = 1,
IgnorePreciseGC = 0,
++ ConcurrentSweep = 0,
src/pkg/runtime/mgc0.c
の冒頭付近にある enum
定義に ConcurrentSweep = 0
が追加されました。これにより、並行スイープがデフォルトで無効化されることを明示的に示し、コードの意図を明確にしています。以前は if(false)
のように直接 false
が書かれていたため、この定数を使うことで、将来的に並行スイープを有効にする際にこの値を 1
に変更するだけで済むようになります。
2. runtime·gc
関数内のファイナライザ実行ロジック
@@ -2237,6 +2238,23 @@ runtime·gc(int32 force)
runtime·semrelease(&runtime·worldsema);
runtime·starttheworld();
m->locks--;
++
++ // now that gc is done, kick off finalizer thread if needed
++ if(!ConcurrentSweep) {
++ if(finq != nil) {
++ runtime·lock(&gclock);
++ // kick off or wake up goroutine to run queued finalizers
++ if(fing == nil)
++ fing = runtime·newproc1(&runfinqv, nil, 0, 0, runtime·gc);
++ else if(fingwait) {
++ fingwait = 0;
++ runtime·ready(fing);
++ }
++ runtime·unlock(&gclock);
++ }
++ // give the queued finalizers, if any, a chance to run
++ runtime·gosched();
++ }
}
runtime·gc
関数は、Goのガベージコレクションのメインエントリポイントの一つです。この変更は、GCの主要な処理(マーク、スイープなど)が完了し、runtime·starttheworld()
によってアプリケーションの実行が再開された直後に行われます。
if(!ConcurrentSweep)
: この条件は、ConcurrentSweep
が0
(false) である場合に真となります。つまり、並行スイープが現在無効になっている(非並行スイープが実行されている)場合にのみ、このブロック内のコードが実行されます。if(finq != nil)
:finq
は、実行を待っているファイナライザがキューされているリストです。この条件は、ファイナライザが登録されている場合に真となります。runtime·lock(&gclock)
/runtime·unlock(&gclock)
:gclock
はGC関連のグローバルなロックです。ファイナライザゴルーチンの状態を変更する際に、競合状態を防ぐためにロックを取得・解放します。if(fing == nil)
:fing
はファイナライザを実行する専用のゴルーチン(finalizer goroutine)へのポインタです。もしこのゴルーチンがまだ起動していない場合、runtime·newproc1(&runfinqv, nil, 0, 0, runtime·gc)
を呼び出して新しいゴルーチンを生成し、runfinqv
関数を実行させます。このゴルーチンがファイナライザキューを処理します。else if(fingwait)
: もしファイナライザゴルーチンが既に存在し、かつfingwait
フラグが真(つまり、ゴルーチンが待機状態にある)の場合、fingwait = 0
で待機状態を解除し、runtime·ready(fing)
でそのゴルーチンを実行可能な状態にします。これにより、待機中のファイナライザゴルーチンがGC完了後にすぐに処理を開始できます。runtime·gosched()
: これは、現在のゴルーチンを一時停止し、Goスケジューラに他の実行可能なゴルーチン(特に、今起動または再開されたファイナライザゴルーチン)にCPUを譲るように指示します。これにより、ファイナライザが迅速に実行される機会が与えられます。
3. 並行スイープ有効化条件の変更
@@ -2384,7 +2402,7 @@ gc(struct gc_args *args)
sweep.spanidx = 0;
// Temporary disable concurrent sweep, because we see failures on builders.
-- if(false) {
++ if(ConcurrentSweep) {
runtime·lock(&gclock);
if(sweep.g == nil)
sweep.g = runtime·newproc1(&bgsweepv, nil, 0, 0, runtime·gc);
この変更は、並行スイープを起動するかどうかを決定する条件を if(false)
から if(ConcurrentSweep)
に変更しています。前述の通り、ConcurrentSweep
は 0
に設定されているため、この条件は常に偽となり、並行スイープは引き続き無効化されたままになります。これは、コードの意図をより明確にし、将来的な並行スイープの再有効化を容易にするための改善です。
関連リンク
- Go CL 62370043: https://golang.org/cl/62370043
参考にした情報源リンク
- Go 1.3におけるGCの改善:
- dev.to: https://dev.to/
- stackoverflow.com: https://stackoverflow.com/
- github.io: https://github.io/
- medium.com (Go 1.5 GC): https://medium.com/
- Goのファイナライザ:
- golang.org: https://golang.org/
- medium.com: https://medium.com/