[インデックス 18731] ファイルの概要
このコミットは、Goランタイムにおけるファイナライザの不安定性(flakiness)を修正するものです。特にテスト環境で顕著だった問題に対処し、runtime.GC
関数の挙動を調整することで、ファイナライザが期待通りに実行されるように改善しています。
コミット
commit 241f63debddc1ceb9a890241a91534a8080117a3
Author: Russ Cox <rsc@golang.org>
Date: Tue Mar 4 09:46:40 2014 -0500
runtime: fix finalizer flakiness
The flakiness appears to be just in tests, not in the actual code.
Specifically, the many tests call runtime.GC once and expect that
the finalizers will be running in the background when GC returns.
Now that the sweep phase is concurrent with execution, however,
the finalizers will not be run until sweep finishes, which might
be quite a bit later. To force sweep to finish, implement runtime.GC
by calling the actual collection twice. The second will complete the
sweep from the first.
This was reliably broken after a few runs before the CL and now
passes tens of runs:
while GOMAXPROCS=2 ./runtime.test -test.run=Finalizer -test.short \
-test.timeout=300s -test.cpu=$(perl -e 'print ("1,2,4," x 100) . "1"')
do true; done
Fixes #7328.
LGTM=dvyukov
R=dvyukov, dave
CC=golang-codereviews
https://golang.org/cl/71080043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/241f63debddc1ceb9a890241a91534a8080117a3
元コミット内容
runtime: fix finalizer flakiness
The flakiness appears to be just in tests, not in the actual code.
Specifically, the many tests call runtime.GC once and expect that
the finalizers will be running in the background when GC returns.
Now that the sweep phase is concurrent with execution, however,
the finalizers will not be run until sweep finishes, which might
be quite a bit later. To force sweep to finish, implement runtime.GC
by calling the actual collection twice. The second will complete the
sweep from the first.
This was reliably broken after a few runs before the CL and now
passes tens of runs:
while GOMAXPROCS=2 ./runtime.test -test.run=Finalizer -test.short \
-test.timeout=300s -test.cpu=$(perl -e 'print ("1,2,4," x 100) . "1"')
do true; done
Fixes #7328.
LGTM=dvyukov
R=dvyukov, dave
CC=golang-codereviews
https://golang.org/cl/71080043
変更の背景
このコミットの主な背景は、Goランタイムのガーベージコレクション(GC)におけるファイナライザの実行タイミングに関するテストの不安定性(flakiness)です。
以前のGoランタイムでは、GCが実行されると、その直後にファイナライザが実行されることが期待されていました。多くのテストコードは、runtime.GC()
を一度呼び出した後、ファイナライザがバックグラウンドで実行されていることを前提としていました。
しかし、GoランタイムのGCのスイープ(sweep)フェーズが、プログラムの実行と並行して行われるように変更されたことで、この前提が崩れました。スイープフェーズは、GCによって不要とマークされたメモリを実際に解放するプロセスです。このスイープが並行して行われるようになったため、runtime.GC()
が返された時点では、まだスイープが完了しておらず、結果としてファイナライザの実行が大幅に遅れる可能性が出てきました。
この遅延が、特にファイナライザの実行に依存するテストにおいて、不安定な結果(テストが時々失敗する)を引き起こしていました。コミットメッセージには、GOMAXPROCS=2
でruntime.test -test.run=Finalizer
を実行すると、以前は頻繁に失敗していたテストが、この変更によって安定してパスするようになったことが示されています。
この問題は、実際のアプリケーションコードで直接的なバグを引き起こすというよりも、テストの信頼性を損なうという側面が強かったようです。開発者は、テストが不安定であると、実際のバグを見逃したり、開発プロセスが遅延したりする可能性があるため、このような不安定性は修正されるべき重要な問題でした。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムとガーベージコレクションに関する前提知識が必要です。
1. Goのガーベージコレクション (GC)
Goは自動メモリ管理を採用しており、ガーベージコレクタが不要になったメモリを自動的に解放します。GoのGCは、主に以下のフェーズで構成されます。
- マーク (Mark) フェーズ: プログラムが使用しているオブジェクト(到達可能なオブジェクト)を特定し、マークします。これは通常、プログラムの実行を一時停止(Stop-the-World: STW)して行われますが、GoのGCはSTW時間を最小限に抑えるように設計されています。
- スイープ (Sweep) フェーズ: マークされなかったオブジェクト(不要なオブジェクト)が占めていたメモリを解放し、再利用可能な状態にします。Go 1.xの特定のバージョン以降では、このスイープフェーズがプログラムの実行と並行して(concurrently)行われるようになりました。これは、GCによるアプリケーションの一時停止時間をさらに短縮するための重要な改善です。
- スイープ終了 (Sweep Termination) フェーズ: 並行スイープが完了するのを待つフェーズです。次のGCサイクルを開始する前に、前のGCサイクルのスイープが完全に終了していることを保証するために必要です。
2. ファイナライザ (Finalizers)
Goでは、runtime.SetFinalizer
関数を使用して、特定のオブジェクトがGCによってメモリから解放される直前に実行される関数(ファイナライザ)を登録できます。ファイナライザは、ファイルハンドルやネットワーク接続などの外部リソースをクリーンアップするために使用されることがあります。
ファイナライザは、GCがオブジェクトを「到達不可能」と判断し、そのメモリが解放される準備ができたときに実行されます。しかし、ファイナライザが実際に実行されるのは、GCのスイープフェーズが完了し、ファイナライザキューが処理されるタイミングです。
3. runtime.GC()
関数
runtime.GC()
関数は、Goプログラムから明示的にガーベージコレクションをトリガーするための関数です。通常、GoのGCは自動的にバックグラウンドで実行されますが、特定の状況(例: メモリ使用量を即座に削減したい場合や、テストでGCの挙動を検証したい場合)でこの関数が呼び出されます。
このコミットの文脈では、runtime.GC()
が呼び出されたときに、ユーザー(特にテストコード)は、その呼び出しが返された時点で、関連するファイナライザが実行されるか、少なくとも実行が開始されることを期待していました。
4. 並行スイープ (Concurrent Sweep) の影響
GoのGCが並行スイープを導入したことで、runtime.GC()
が呼び出されてGCが実行されても、スイープフェーズがバックグラウンドで非同期に進行するようになりました。これはアプリケーションの応答性向上には寄与しますが、runtime.GC()
が返った時点では、まだメモリの解放が完了しておらず、ファイナライザの実行も保留されている可能性があることを意味します。
この「スイープの遅延」が、ファイナライザの実行タイミングに依存するテストの不安定性の根本原因でした。テストはruntime.GC()
の呼び出し後すぐにファイナライザが実行されることを期待しているのに、実際にはスイープが完了するまで待たされるため、アサーションが失敗することがあったのです。
技術的詳細
このコミットの技術的な解決策は、runtime.GC()
関数の内部実装を変更し、実際のガーベージコレクション処理を2回連続で呼び出すというものです。
具体的には、runtime.GC()
関数は、内部のruntime·gc(1)
関数を2回呼び出すように変更されました。
-
1回目の
runtime·gc(1)
呼び出し:- これは通常のガーベージコレクションサイクルを実行します。
- マークフェーズが完了し、スイープフェーズが開始されます。
- しかし、スイープフェーズは並行して実行されるため、この呼び出しが返った時点では、スイープはまだ完了していない可能性があります。
- この時点で、ファイナライザの実行は、スイープが完了するまで待機状態になります。
-
2回目の
runtime·gc(1)
呼び出し:- この2回目の呼び出しが、問題の解決に不可欠な部分です。
- GoのGCの設計上、新しいGCサイクルを開始する前には、前のGCサイクルのスイープフェーズが完全に完了している必要があります。これは「スイープ終了フェーズ」として知られています。
- したがって、2回目の
runtime·gc(1)
が呼び出されると、システムは1回目のGCサイクルで開始された並行スイープが完全に終了するまで待機します。 - スイープが完了すると、ファイナライザが実行される準備が整います。
- その後、2回目のGCサイクルが開始されますが、この2回目のコレクション自体は、ユーザーが
runtime.GC()
を呼び出した目的(メモリ解放とファイナライザ実行の保証)からすると「過剰(overkill)」であるとコミットメッセージに記載されています。しかし、これにより、ユーザーがruntime.GC()
から戻った時点で、ファイナライザが確実に実行されている状態を保証できます。
このアプローチは、runtime.GC()
のセマンティクスを、ユーザーが期待する「未使用メモリが解放され、ファイナライザが実行された状態」に合わせるためのものです。特にテストコードのように、runtime.GC()
を呼び出した直後にファイナライザの副作用を期待するシナリオにおいて、この変更はテストの信頼性を大幅に向上させます。
この修正は、実際のアプリケーションのパフォーマンスにわずかなオーバーヘッドをもたらす可能性があります(不要な2回目のGCサイクルが実行されるため)。しかし、コミットメッセージにあるように、「ユーザーがruntime.GC
を呼び出す正当な理由があり、そのコストを許容できる」という前提のもと、テストの安定性というメリットがこのオーバーヘッドを上回ると判断されています。
コアとなるコードの変更箇所
変更は src/pkg/runtime/malloc.goc
ファイルの GC()
関数に集中しています。
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -803,6 +803,15 @@ runtime·cnewarray(Type *typ, intgo n)
}
func GC() {
+ // We assume that the user expects unused memory to have
+ // been freed when GC returns. To ensure this, run gc(1) twice.
+ // The first will do a collection, and the second will force the
+ // first's sweeping to finish before doing a second collection.
+ // The second collection is overkill, but we assume the user
+ // has a good reason for calling runtime.GC and can stand the
+ // expense. At the least, this fixes all the calls to runtime.GC in
+ // tests that expect finalizers to start running when GC returns.
+ runtime·gc(1);
runtime·gc(1);
}
コアとなるコードの解説
変更されたGC()
関数は、Goのランタイム内部でガーベージコレクションをトリガーするエントリポイントの一つです。この関数は、Goプログラムからruntime.GC()
として呼び出されます。
変更前は、GC()
関数はruntime·gc(1)
を一度だけ呼び出していました。
変更後は、以下のようになっています。
func GC() {
// We assume that the user expects unused memory to have
// been freed when GC returns. To ensure this, run gc(1) twice.
// The first will do a collection, and the second will force the
// first's sweeping to finish before doing a second collection.
// The second collection is overkill, but we assume the user
// has a good reason for calling runtime.GC and can stand the
// expense. At the least, this fixes all the calls to runtime.GC in
// tests that expect finalizers to start running when GC returns.
runtime·gc(1); // 1回目のGC呼び出し
runtime·gc(1); // 2回目のGC呼び出し
}
runtime·gc(1)
: これはGoランタイムの内部関数で、実際のガーベージコレクションプロセスを開始します。引数の1
は、GCのタイプやモードを示すものと考えられます(詳細な意味はランタイムの他の部分で定義されますが、ここではGCをトリガーするという意味で理解できます)。
このコードの核心は、runtime·gc(1)
が2回連続で呼び出されている点です。
-
1回目の
runtime·gc(1)
:- 通常のGCサイクルを開始します。これにはマークフェーズと、並行して実行されるスイープフェーズが含まれます。
- 並行スイープのため、この呼び出しが返った時点では、メモリの解放(スイープ)が完全に終わっていない可能性があります。
- ファイナライザは、関連するメモリがスイープによって解放された後に実行されるため、この時点ではまだ実行されていない可能性があります。
-
2回目の
runtime·gc(1)
:- GoのGCメカニズムの重要な特性として、新しいGCサイクルを開始する前に、前のGCサイクルのスイープフェーズが完全に完了していることを保証する必要があります。
- したがって、この2回目の
runtime·gc(1)
が呼び出されると、ランタイムは1回目のruntime·gc(1)
によって開始された並行スイープが終了するまで待機します。 - スイープが完了すると、ファイナライザが実行される準備が整い、ファイナライザキューが処理されます。
- その後、2回目のGCサイクル自体が実行されますが、これは主に1回目のスイープを強制的に完了させるための副作用として利用されています。コミットメッセージにあるように、この2回目のコレクションは「過剰(overkill)」ですが、
runtime.GC()
の呼び出し元が期待する状態(メモリが解放され、ファイナライザが実行された状態)を保証するためのトレードオフです。
この変更により、runtime.GC()
が返ったときには、ファイナライザが確実に実行されている(または実行が開始されている)状態が保証され、特にファイナライザの実行タイミングに敏感なテストの不安定性が解消されました。
関連リンク
- Go Issue #7328: https://github.com/golang/go/issues/7328
- このコミットが修正した具体的な問題のトラッキングイシューです。通常、イシューページには問題の詳細な説明、議論、再現手順などが記載されています。
- Gerrit Change-ID 71080043: https://golang.org/cl/71080043
- GoプロジェクトではGerritがコードレビューシステムとして使用されており、このリンクはコミットに至るまでのコードレビューのやり取り、提案された変更、コメント、承認履歴などを確認できます。
参考にした情報源リンク
- Goの公式ドキュメント(ガーベージコレクション、ファイナライザに関するセクション)
- Goのソースコード(特に
src/runtime
ディレクトリ内のGC関連ファイル) - Goのブログ記事やカンファレンス発表(GoのGCの進化に関するもの)
- GoのイシューとGerritの変更リスト(上記「関連リンク」に記載)
- Goのテストフレームワークに関するドキュメント(
runtime.test
の挙動を理解するため) - Goのメモリ管理に関する技術記事や解説記事
(注: 上記の参考情報源は一般的なものであり、この解説を生成する際に具体的に参照した個別のURLをすべて列挙しているわけではありません。GoのGCやランタイムに関する一般的な知識と、提供されたコミット情報、関連リンクから得られる情報を総合して解説を作成しています。)