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

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

このコミットは、Goランタイムのtimerproc関数におけるメモリリークの可能性を解消することを目的としています。具体的には、タイマー関数とその引数が、次のタイマーが実行されるまでのスリープ中に適切にクリアされないことで発生するオブジェクトの参照保持を防ぎ、ガベージコレクションによる回収を可能にします。

コミット

commit be1c71ecb5b02220b282884317908b4da89ef37a
Author: Russ Cox <rsc@golang.org>
Date:   Mon Feb 17 20:11:53 2014 -0500

    runtime: clear f, arg to avoid leak in timerproc
    
    I have seen this cause leaks where not all objects in a sync.Pool
    would be reclaimed during the sync package tests.
    I found it while debugging the '0 of 100 finalized' failure we are
    seeing on arm, but it seems not to be the root cause for that one.
    
    LGTM=dave, dvyukov
    R=golang-codereviews, dave, dvyukov
    CC=golang-codereviews
    https://golang.org/cl/64920044

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

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

元コミット内容

runtime: clear f, arg to avoid leak in timerproc

このコミットは、timerprocにおけるリークを避けるために、関数fと引数argをクリアします。

sync.Poolのテスト中に、すべてのオブジェクトが回収されないリークが発生するのを確認しました。 これは、armアーキテクチャで発生している「0 of 100 finalized」エラーのデバッグ中に発見されましたが、その根本原因ではないようです。

変更の背景

このコミットの背景には、Goランタイムにおけるメモリ管理の最適化と、特定の条件下でのメモリリークの解消という重要な課題がありました。コミットメッセージによると、主な動機は以下の2点です。

  1. sync.Poolテストにおけるオブジェクトの回収漏れ: sync.Poolは、オブジェクトの再利用を促進し、ガベージコレクションの負荷を軽減するためのメカニズムです。しかし、テスト中にsync.Pool内のオブジェクトが完全に回収されないリークが確認されました。これは、timerprocがタイマー関数とその引数を保持し続けることで、これらのオブジェクトへの参照が残り、ガベージコレクタがそれらを解放できないために発生していました。
  2. ARMアーキテクチャでの「0 of 100 finalized」エラーのデバッグ: ARMアーキテクチャで発生していた「0 of 100 finalized」というエラーは、ファイナライザが期待通りに実行されない、またはオブジェクトが最終化されない問題を示唆していました。このコミットの変更は、この問題の直接的な原因ではなかったものの、デバッグの過程でtimerprocにおけるメモリリークの可能性が発見されました。

これらの問題は、Goプログラムの長期的な安定性とメモリ効率に影響を与える可能性があったため、timerprocがタイマー実行後に不要な参照を保持しないように修正する必要がありました。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念とメモリ管理の仕組みについて理解しておく必要があります。

1. timerproc

timerprocは、Goランタイム内部で動作する特別なゴルーチン(Goの軽量スレッド)です。その主な役割は、time.AfterFunctime.NewTimerなどで設定されたすべてのタイマーを管理し、期限が来たタイマーのコールバック関数を実行することです。

  • タイマーの管理: timerprocは、タイマーを効率的に管理するために、最小ヒープ(min-heap)データ構造を使用します。これにより、次に期限が来るタイマーを素早く特定できます。
  • 実行ループ: timerprocは継続的にタイマーヒープを監視し、期限が来たタイマーがあれば、そのタイマーに関連付けられた関数を実行します。タイマーがすぐに期限切れにならない場合、timerprocnotetsleepggoparkのようなメカニズムを使用して自身をスリープさせ、新しいタイマーが追加されたり、既存のタイマーが期限切れになったりしたときにウェイクアップされます。
  • Go 1.14以降の変更: Go 1.14以降では、明示的なtimerprocの非同期タスクは削除され、タイマーのスケジューリングと実行の責任はGoスケジューラのメインループまたはシステム監視に直接統合されました。これにより、コンテキストスイッチのオーバーヘッドが削減され、タイマーの応答性が向上しました。ただし、このコミットが作成された時点(Go 1.2以前)では、timerprocは独立したゴルーチンとして存在していました。

2. sync.Pool

sync.Poolは、Goの標準ライブラリsyncパッケージが提供する型で、一時的なオブジェクトの再利用を目的としています。

  • 目的: sync.Poolの主な目的は、頻繁に作成および破棄される一時的なオブジェクトのメモリ割り当てとガベージコレクションのオーバーヘッドを削減することです。これにより、アプリケーションのパフォーマンスが向上し、GCポーズが短縮されます。
  • 動作: sync.Poolは、Get()メソッドでオブジェクトを取得し、Put()メソッドでオブジェクトをプールに戻します。プールに戻されたオブジェクトは、将来のGet()呼び出しで再利用される可能性があります。
  • GCとの連携: sync.PoolはGoのガベージコレクタと連携して動作します。GCサイクルが開始される前に、ランタイムはsync.Poolインスタンスを追跡するallPoolsスライスをクリアするクリーンアッププロセスを開始します。オブジェクトは「victim area」に移動され、2回のGCサイクル後に完全に破棄されます。これにより、プールが無限に成長するのを防ぎます。
  • 注意点: sync.Poolはキャッシュメカニズムではなく、一時的なオブジェクトの再利用に特化しています。オブジェクトをGet()で取得した後、Put()でプールに戻し忘れると、メモリ使用量が増加し、リークのように見えることがあります。また、再利用する前にオブジェクトの内部状態を適切にリセットする必要があります。

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

Goのメモリ管理は、高度なガベージコレクタ(GC)によって自動化されています。

  • スタックとヒープ: Goプログラムは、スタックとヒープの2つの主要な場所にメモリを割り当てます。
    • スタックメモリ: ローカル変数や関数呼び出しデータに使用され、関数が終了すると自動的に解放されます。高速です。
    • ヒープメモリ: コンパイル時にサイズが不明な動的に割り当てられたデータや、関数のライフサイクルを超えて存続する必要があるデータに使用されます。ガベージコレクタによって管理されます。
  • エスケープ解析: Goのコンパイラは、エスケープ解析という技術を使用して、変数がスタックに割り当てられるべきか、ヒープに割り当てられるべきかを決定します。
  • GCアルゴリズム: GoのGCは、並行、トライカラー、マーク&スイープアルゴリズムを採用しています。
    • 並行: GCのほとんどの作業はアプリケーションと並行して実行され、「ストップ・ザ・ワールド」(STW)ポーズ(GC操作のためにアプリケーションの実行が一時的に停止する短い時間)を最小限に抑えます。
    • トライカラーマーキング: オブジェクトを白(未マーク、回収候補)、灰色(訪問済みだが参照先未スキャン)、黒(訪問済み、参照先もスキャン済み、到達可能)の3色に分類します。
    • マーク&スイープ: マーキングフェーズで到達可能なオブジェクト(黒)を特定した後、スイープフェーズで未マーク(白)のオブジェクトが占めるメモリを回収します。GoのGCは非世代別(オブジェクトを年齢で分類しない)であり、非コンパクション(メモリ内のオブジェクトを再配置しない)です。
  • ファイナライザ: Goでは、runtime.SetFinalizer関数を使用して、オブジェクトがガベージコレクションされる直前に実行されるファイナライザを設定できます。これは、外部リソースの解放などのクリーンアップ操作に使用されます。コミットメッセージの「0 of 100 finalized」は、ファイナライザの実行に関する問題を示唆しています。

技術的詳細

このコミットの技術的詳細は、timerprocがタイマー関数を実行した後、その関数と引数への参照を明示的にnilに設定することで、メモリリークを防ぐという点に集約されます。

timerprocは、タイマーが期限切れになったときに実行される関数fと、その関数に渡される引数argを内部的に保持しています。タイマーが実行された後、timerprocは次のタイマーの期限が来るまでスリープ状態に入ることがあります。このスリープ中に、もしfargがクリアされずに残っていると、それらが参照しているオブジェクト(例えば、sync.Poolから取得されたオブジェクトや、クロージャによってキャプチャされた変数など)は、ガベージコレクタによって到達可能と判断され、回収されません。

特に、sync.Poolのテストでリークが確認されたのは、timerprocsync.Poolから取得されたオブジェクトを引数として受け取り、そのオブジェクトへの参照を保持し続けたためと考えられます。sync.Poolは、GCサイクル中にプール内のオブジェクトをクリーンアップしますが、もし外部から参照されているオブジェクトがあれば、それは回収の対象外となります。

このコミットでは、タイマー関数fが実行された直後に、f自体をnilに、そしてargの内部データポインタをnilに設定しています。これにより、timerprocがこれらのオブジェクトへの参照を解放し、ガベージコレクタがそれらを適切に回収できるようになります。

USED(f)USED(&arg)というマクロは、Goのコンパイラがこれらのnil割り当てを最適化によって削除しないようにするためのものです。Goのコンパイラは、変数がその後使用されないと判断した場合、その変数の割り当てを削除することがあります。しかし、このケースでは、nilを割り当てること自体がメモリリークを防ぐための重要な操作であるため、コンパイラにこの割り当てを保持させる必要があります。USEDマクロは、変数が「使用されている」とマークすることで、この最適化を防ぎます。

この修正は、timerprocが長時間稼働するシステムにおいて、微細なメモリリークが蓄積し、最終的にメモリ使用量の増加やパフォーマンスの低下を引き起こす可能性を排除します。また、sync.Poolのようなメモリ最適化メカニズムが意図通りに機能することを保証します。

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

変更はsrc/pkg/runtime/time.gocファイルにあります。

--- a/src/pkg/runtime/time.goc
+++ b/src/pkg/runtime/time.goc
@@ -217,6 +217,14 @@ timerproc(void)
 		if(raceenabled)
 			runtime·raceacquire(t);
 		f(now, arg);
+
+		// clear f and arg to avoid leak while sleeping for next timer
+		f = nil;
+		USED(f);
+		arg.type = nil;
+		arg.data = nil;
+		USED(&arg);
+
 		runtime·lock(&timers);
 	}
 	if(delta < 0) {

コアとなるコードの解説

追加されたコードは以下の部分です。

			// clear f and arg to avoid leak while sleeping for next timer
			f = nil;
			USED(f);
			arg.type = nil;
			arg.data = nil;
			USED(&arg);
  1. f = nil;

    • fは、実行されたタイマーのコールバック関数へのポインタです。
    • この行は、関数が実行された後、fが保持していた関数オブジェクトへの参照をnilに設定します。これにより、fが参照していた関数オブジェクトがガベージコレクションの対象となり、メモリが解放される可能性が高まります。
  2. USED(f);

    • USEDマクロは、Goランタイムの内部で使われるもので、コンパイラが変数の使用を最適化によって削除しないようにするためのヒントです。
    • f = nilという代入が、その後のコードでfが使われないとコンパイラが判断した場合に削除されてしまうことを防ぎます。このnil代入自体がメモリリークを防ぐための重要な操作であるため、削除されてはなりません。
  3. arg.type = nil;

    • argは、タイマー関数fに渡される引数です。Goランタイム内部では、引数はinterface{}型のように扱われ、その実体はtypedataの2つのフィールドで表現されます。typeフィールドは引数の型情報を保持します。
    • この行は、引数の型情報への参照をnilに設定します。
  4. arg.data = nil;

    • dataフィールドは、引数の実際の値(データ)へのポインタを保持します。
    • この行は、引数のデータへの参照をnilに設定します。これにより、argが参照していた実際のデータオブジェクトがガベージコレクションの対象となります。
  5. USED(&arg);

    • USED(f)と同様に、argのフィールドへのnil代入がコンパイラによって削除されないようにするためのヒントです。argは構造体であるため、そのアドレス&argUSEDに渡しています。

これらの変更により、timerprocがタイマー関数を実行し終えた後、その関数と引数に関連するオブジェクトへの参照を積極的に解放します。これにより、これらのオブジェクトがガベージコレクタによって適切に回収され、特にsync.Poolのような再利用メカニズムと組み合わせた場合に発生する可能性のあるメモリリークが防止されます。

関連リンク

参考にした情報源リンク