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

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

このコミットは、Go言語の標準ライブラリである sync パッケージ内の pool_test.go ファイルに対する変更です。具体的には、TestPoolGC というテスト関数における time.Sleep の挙動が修正されています。

コミット

commit e721778f3ed43b5ec6beda5e3f2fa93eda38f352
Author: Russ Cox <rsc@golang.org>
Date:   Fri Mar 7 16:08:12 2014 -0500

    sync: give finalizers more time in TestPoolGC
    
    If we report a leak, make sure we've waited long enough to be sure.
    The new sleep regimen waits 1.05 seconds before failing; the old
    one waited 0.005 seconds.
    
    (The single linux/amd64 failure in this test feels more like a
    timing problem than a leak. I don't want to spend time on it unless
    we're sure.)
    
    LGTM=bradfitz
    R=bradfitz
    CC=golang-codereviews
    https://golang.org/cl/72630043

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

https://github.com/golang/go/commit/e721778f3ed43b5ec6beda5e3f2fa93eda38f352

元コミット内容

sync: give finalizers more time in TestPoolGC

このコミットは、TestPoolGC テストにおいてファイナライザが実行されるための時間をより長く与えることを目的としています。もしリークが報告された場合、それが本当にリークであると確信できるまで十分に待機するように変更されました。新しいスリープの仕組みでは、失敗するまでに最大1.05秒待機しますが、以前は0.005秒しか待機していませんでした。

(このテストにおける単一の linux/amd64 での失敗は、リークというよりもタイミングの問題のように感じられます。確信が持てない限り、これに時間を費やしたくありません。)

変更の背景

この変更の背景には、Go言語の sync.Pool のテストである TestPoolGC が、特定の環境(特に linux/amd64)で不安定な失敗(flaky failure)を示していた問題があります。コミットメッセージによると、この失敗はメモリリークではなく、ガベージコレクション(GC)とファイナライザの実行タイミングに起因するものである可能性が高いと推測されています。

TestPoolGC は、sync.Pool がキャッシュしたオブジェクトがGCによって適切に解放されることを検証するためのテストです。このテストでは、runtime.GC() を明示的に呼び出してGCサイクルをトリガーし、その後、プール内のオブジェクトの状態をアサートします。しかし、Goのガベージコレクションは非決定論的であり、アプリケーションと並行して実行されます。runtime.GC() を呼び出しても、GCサイクルが即座に完了したり、ファイナライザがすぐに実行される保証はありません。ファイナライザはGCがオブジェクトを到達不能と判断した後に実行されるため、その実行にはある程度の時間差が生じる可能性があります。

以前のテストでは、GC呼び出し後に time.Sleep(time.Millisecond)、つまりわずか1ミリ秒しか待機していませんでした。この短い待機時間では、GCが完了し、関連するファイナライザが実行されるのに十分な時間が確保されない場合があり、結果としてテストが誤ってリークを報告してしまう可能性がありました。コミットメッセージにある「タイミングの問題」とは、まさにこのGCとファイナライザの非同期性によるテストの不安定さを指しています。

このコミットは、テストの信頼性を向上させ、実際のリークとタイミングに起因する誤検出を区別するために、ファイナライザが実行されるための十分な猶予期間を設けることを目的としています。

前提知識の解説

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

Go言語は、自動メモリ管理のためにガベージコレクタ(GC)を採用しています。開発者は手動でメモリを解放する必要がなく、GCが不要になったメモリを自動的に回収します。GoのGCは並行(concurrent)かつ低遅延(low-latency)なマーク&スイープ方式をベースとしています。

  • 並行性: GCはアプリケーションの実行と並行して動作します。これにより、GCによるアプリケーションの一時停止(Stop-the-World)時間を最小限に抑え、全体的なスループットを向上させます。
  • 非決定論性: GCがいつ実行されるか、どのオブジェクトがいつ回収されるかは、プログラムの実行中に厳密に予測することはできません。GCはヒューリスティックに基づいて動作し、メモリ使用量や割り当てレートなどの要因によってトリガーされます。
  • runtime.GC(): runtime.GC() 関数を呼び出すことで、GCサイクルを明示的に要求できます。しかし、これはGCの実行を「要求」するものであり、即座に完了を「保証」するものではありません。特に、ファイナライザの実行はGCがオブジェクトを到達不能と判断した後に行われるため、さらに時間差が生じる可能性があります。

ファイナライザ (runtime.SetFinalizer)

Goのファイナライザは、runtime.SetFinalizer 関数を使用してオブジェクトに関連付けられる関数です。この関数は、GCがそのオブジェクトを到達不能と判断し、メモリを回収する直前に実行されるようにスケジュールされます。

  • 目的: ファイナライザの主な目的は、ファイルディスクリプタ、ネットワーク接続、データベースハンドルなど、GoのGCが直接管理できない非メモリリソースをクリーンアップすることです。
  • 非決定論性: ファイナライザの実行もまた非決定論的です。GCがオブジェクトを回収するタイミングに依存するため、いつ実行されるかは保証されません。また、プログラムが終了する前にファイナライザが実行される保証もありません。
  • 注意点: ファイナライザは、その非決定論的な性質と、オブジェクトのライフサイクルを延長したり、意図しないメモリリークを引き起こす可能性(特に循環参照がある場合)があるため、慎重に使用する必要があります。Goでは、リソースのクリーンアップには defer ステートメントや明示的な Close メソッドの使用が推奨されます。

sync.Pool

sync.Pool は、Goの sync パッケージが提供する型で、一時的なオブジェクトを再利用するために使用されます。その主な目的は、特に高スループットの並行アプリケーションにおいて、短命なオブジェクトの繰り返しのアロケーションとデアロケーションのコストを削減することです。

  • メモリ割り当てのオーバーヘッド削減: 新しいオブジェクトを毎回作成する代わりに、sync.Pool は既存のオブジェクトの再利用を可能にします。これにより、ヒープ上でのオブジェクトの繰り返し作成を回避し、メモリ割り当てのオーバーヘッドを削減します。
  • GCプレッシャーの軽減: 割り当てが少なくなると、Goのガベージコレクタの作業が直接的に減少します。オブジェクトを再利用することで、sync.Pool はGCプレッシャーを軽減し、GCサイクルを減らし、一時停止時間を短縮します。これは、パフォーマンスが重要なシナリオで非常に重要です。
  • スレッドセーフ: sync.Pool は、従来のロックメカニズムなしに、ゴルーチン間でオブジェクトをキャッシュおよび再利用するためのスレッドセーフな方法を提供します。これにより、ロックの競合を最小限に抑え、高い並行性を実現します。

sync.Pool に格納されるオブジェクトは一時的なものであり、GCによっていつでもクリアされる可能性があることに注意が必要です。したがって、永続的なオブジェクトの保存には使用すべきではありません。

技術的詳細

TestPoolGC は、sync.Pool がガベージコレクションによって適切にクリーンアップされることを検証するテストです。このテストでは、sync.Pool に多数のオブジェクトを投入し、その後 runtime.GC() を複数回呼び出してGCを強制的に実行します。そして、ファイナライザが実行された回数をカウントし、プールされたオブジェクトが適切に回収されたことを確認します。

問題は、以前のコードがGC呼び出し後に time.Sleep(time.Millisecond) という非常に短い時間しか待機していなかった点にありました。GoのGCは並行して動作し、ファイナライザの実行はGCがオブジェクトを到達不能と判断した後に非同期的に行われます。特に負荷の高いシステムや特定の環境(linux/amd64 で報告されたように)では、1ミリ秒という時間ではGCが完了し、すべてのファイナライザが実行されるのに十分ではない場合がありました。

これにより、テストが fin カウンタ(ファイナライザが実行された回数を追跡する)が期待値に達する前にタイムアウトし、誤ってメモリリークを報告してしまうという「タイミングの問題」が発生していました。これは実際のリークではなく、テストの検証ロジックがGCの非同期性を十分に考慮していなかったために生じる偽陽性でした。

このコミットでは、このタイミングの問題を解決するために、time.Sleep の待機時間を動的に、かつ大幅に延長するように変更されました。これにより、GCとファイナライザがその作業を完了するための十分な時間が確保され、テストの信頼性が向上します。

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

--- a/src/pkg/sync/pool_test.go
+++ b/src/pkg/sync/pool_test.go
@@ -87,7 +87,7 @@ func TestPoolGC(t *testing.T) {
 	}
 	for i := 0; i < 5; i++ {
 		runtime.GC()
-		time.Sleep(time.Millisecond)
+		time.Sleep(time.Duration(i*100+10) * time.Millisecond)
 		// 1 pointer can remain on stack or elsewhere
 		if atomic.LoadUint32(&fin) >= N-1 {
 			return

コアとなるコードの解説

変更は src/pkg/sync/pool_test.go ファイルの TestPoolGC 関数内で行われています。

元のコードでは、GCを呼び出した後に固定の time.Sleep(time.Millisecond)、つまり1ミリ秒だけ待機していました。

time.Sleep(time.Millisecond)

このコミットでは、この行が以下のように変更されました。

time.Sleep(time.Duration(i*100+10) * time.Millisecond)

この変更のポイントは以下の通りです。

  1. 動的な待機時間: i はループ変数であり、0から4まで変化します。これにより、time.Sleep の待機時間がループの各イテレーションで増加します。

    • i = 0: time.Sleep(time.Duration(0*100+10) * time.Millisecond) = time.Sleep(10 * time.Millisecond) (10ミリ秒)
    • i = 1: time.Sleep(time.Duration(1*100+10) * time.Millisecond) = time.Sleep(110 * time.Millisecond) (110ミリ秒)
    • i = 2: time.Sleep(time.Duration(2*100+10) * time.Millisecond) = time.Sleep(210 * time.Millisecond) (210ミリ秒)
    • i = 3: time.Sleep(time.Duration(3*100+10) * time.Millisecond) = time.Sleep(310 * time.Millisecond) (310ミリ秒)
    • i = 4: time.Sleep(time.Duration(4*100+10) * time.Millisecond) = time.Sleep(410 * time.Millisecond) (410ミリ秒)
  2. 合計待機時間: ループ全体での合計待機時間は、10 + 110 + 210 + 310 + 410 = 1050ミリ秒、つまり1.05秒になります。これは、元の0.005秒(5ミリ秒)と比較して大幅な増加です。

  3. 目的: この動的かつ大幅な待機時間の延長により、GCがオブジェクトを回収し、関連するファイナライザが実行されるための十分な時間が確保されます。これにより、GCの非同期性やシステム負荷によるタイミングのずれが吸収され、テストが誤ってリークを報告する可能性が低減されます。コミットメッセージにあるように、「リークを報告する場合、十分に長く待機したことを確認する」という目的が達成されます。

この変更は、GoのテストがGCの非決定論的な性質をより適切に扱うための実用的なアプローチを示しています。テストの信頼性を高めるために、実際のシステム動作を考慮した待機時間を設定することが重要であるという教訓を反映しています。

関連リンク

参考にした情報源リンク