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

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

このコミットは、Goランタイムのガベージコレクション(GC)に関連するsrc/pkg/runtime/mgc0.cファイルに対するものです。mgc0.cは、Goランタイムの初期のGC実装の一部を担っていたC言語のソースファイルであり、特にバックグラウンドでのスイープ処理(bgsweep)やファイナライザの実行(runfinq)といった重要な機能を含んでいました。現在のGoランタイムでは、GCのコア部分はGo言語で再実装され、主にsrc/runtime/mgc.goなどのファイルに移行していますが、このコミットが作成された時点ではmgc0.cがGCの重要なコンポーネントでした。

コミット

  • コミットハッシュ: fed5428c4aca483ceec8a6cdeac5c80098a30e64
  • Author: Dmitriy Vyukov dvyukov@google.com
  • Date: Fri Mar 14 23:32:12 2014 +0400

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

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

元コミット内容

runtime: fix another race in bgsweep
It's possible that bgsweep constantly does not catch up for some reason,
in this case runfinq was not woken at all.

R=rsc
CC=golang-codereviews
https://golang.org/cl/75940043

変更の背景

このコミットは、Goランタイムのガベージコレクション(GC)における既知の競合状態(race condition)を修正することを目的としています。具体的には、バックグラウンドスイープ(bgsweep)が何らかの理由でGCサイクルに追いつかない場合に、ファイナライザを処理するゴルーチン(runfinq)が全く起動されない、またはウェイクアップされないという問題がありました。

GoのGCは、メモリの解放をバックグラウンドで行うことで、アプリケーションの実行を可能な限り中断しないように設計されています。bgsweepは、GCのマークフェーズで到達不能と判断されたオブジェクトが占めるメモリを実際に解放する役割を担います。一方、ファイナライザは、オブジェクトがGCによって回収される直前に実行される特定の関数であり、ファイルディスクリプタやネットワーク接続などの非メモリリソースのクリーンアップによく使用されます。

問題は、bgsweepが遅延し、次のGCサイクルが開始されてもまだ前のサイクルのスイープが完了していないような状況で発生しました。このような場合、runfinqゴルーチンを起動またはウェイクアップするロジックが適切にトリガーされず、結果としてファイナライザが実行されない可能性がありました。これは、リソースリークやアプリケーションの予期せぬ動作につながる重大な問題です。このコミットは、bgsweepの進行状況に関わらず、ファイナライザが確実に処理されるようにするための修正を導入しています。

前提知識の解説

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

GoのGCは、自動メモリ管理システムであり、プログラムが使用しなくなったメモリを自動的に回収します。GoのGCは以下の特徴を持ちます。

  • 並行性 (Concurrent): GCの大部分の処理は、アプリケーションのゴルーチンと並行して実行されます。これにより、プログラムの実行が停止する「Stop-The-World (STW)」ポーズの時間を最小限に抑え、アプリケーションの応答性を向上させます。
  • トライカラーマーク&スイープ (Tri-color Mark-Sweep): GoのGCは、トライカラーアルゴリズムに基づくマーク&スイープ方式を採用しています。
    • マークフェーズ: GCは、プログラムから到達可能なオブジェクト(ライブオブジェクト)を特定します。オブジェクトは白(未マーク、潜在的なゴミ)、灰(マーク済みだが参照先が未スキャン)、黒(マーク済みで参照先もスキャン済み)の3色で分類されます。
    • スイープフェーズ: マークフェーズの後に、到達不能と判断されたオブジェクトが占めるメモリを解放します。
  • 非世代別 (Non-generational): 多くのGCシステムとは異なり、GoのGCはオブジェクトを世代別に分類しません(例:Young世代、Old世代)。すべてのオブジェクトは均等に扱われます。
  • 非圧縮 (Non-compacting): GoのGCは、メモリ内のオブジェクトを移動させません。これにより、オブジェクトの移動と参照の更新に伴うオーバーヘッドが回避されますが、メモリの断片化が発生する可能性があります。
  • ライトバリア (Write Barriers): 並行マークフェーズ中にオブジェクト参照の変更を追跡し、ライブオブジェクトが誤って回収されないようにするために使用されます。

bgsweep (バックグラウンドスイープ)

bgsweepは、Goランタイム内でバックグラウンドで動作するゴルーチンであり、GCのスイープフェーズを担当します。マークフェーズで到達不能とマークされたメモリ領域を実際に解放し、再利用可能な状態にします。bgsweepは低優先度で実行され、runtime.Gosched()を呼び出してCPU時間を他のゴルーチンに譲ることで、アプリケーションのパフォーマンスへの影響を最小限に抑えます。しかし、これによりbgsweepがGCサイクルに追いつかない状況が発生する可能性もあります。

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

Goのファイナライザは、runtime.SetFinalizer関数を使用してオブジェクトに関連付けられる関数です。GCがオブジェクトが到達不能であると判断した後、そのオブジェクトがメモリから回収される前に、関連付けられたファイナライザ関数が実行されます。ファイナライザは、主にファイルディスクリプタ、ネットワーク接続、C言語で割り当てられたメモリなど、Goのヒープ外のリソースをクリーンアップするための「安全ネット」として使用されます。

ファイナライザの実行は保証されず、いつ実行されるかも予測できません。プログラムが終了する前に実行されない可能性もあります。そのため、重要なリソース管理にはdefer文やio.Closerインターフェースのような明示的なクリーンアップメカニズムが推奨されます。

ファイナライザは、runfinqというゴルーチンによって処理されます。runfinqは、ファイナライザキューに積まれたファイナライザを順次実行する役割を担います。

技術的詳細

このコミットが修正しようとしている問題は、bgsweeprunfinqの間の同期に関するものです。

従来のロジックでは、bgsweepがスイープ処理を完了した後、またはGCが完了した後に、finq(ファイナライザキュー)にファイナライザが存在する場合にのみrunfinqゴルーチンを起動またはウェイクアップしていました。

しかし、bgsweepが何らかの理由(例えば、大量のメモリ解放が必要で時間がかかる、または他の高優先度ゴルーチンにCPU時間を奪われるなど)で遅延し、次のGCサイクルが開始されてもまだスイープが完了していない場合、bgsweepruntime.sweepone()-1を返すまでループし続けます。このループ中にrunfinqをウェイクアップする機会が失われる可能性がありました。

このコミットでは、この問題を解決するために以下の変更が導入されています。

  1. wakefing関数の導入: ファイナライザを処理するゴルーチン(runfinq)を起動またはウェイクアップするための専用関数wakefingが導入されました。この関数は、finqにファイナライザが存在する場合に、fingゴルーチン(runfinqを実行するゴルーチン)がまだ存在しない場合は新規作成し、既に存在して待機状態(fingwaitがtrue)であればウェイクアップします。
  2. bgsweep内でのwakefingの呼び出し:
    • bgsweepのループ内で、runtime.sweepone()が呼び出されるたびに、現在のスイープ世代(runtime.mheap.sweepgen)がsweep.lastsweepgenと異なる場合にwakefing()が呼び出されるようになりました。これは、bgsweepがGCに追いついていない場合でも、少なくともGCサイクルごとに一度はrunfinqを起動またはウェイクアップすることを保証します。
    • bgsweepのスイープループが完了した後(while(runtime·sweepone() != -1)のループを抜けた後)にもwakefing()が呼び出されるようになりました。これにより、スイープが完了した時点でファイナライザが確実に処理されるようになります。
  3. runtime·gc内でのwakefingの呼び出し:
    • GCが完了した後、ConcurrentSweepがfalseの場合(つまり、並行スイープが有効でない場合)、従来の複雑なfinqチェックとfingの起動/ウェイクアップロジックがwakefing()の呼び出しに置き換えられました。これにより、GC完了後にもファイナライザが確実に処理されるようになります。

これらの変更により、bgsweepの進行状況やGCの完了状態に関わらず、ファイナライザが適切なタイミングで処理されることが保証され、競合状態が解消されます。特に、sweep.lastsweepgenの導入は、bgsweepが遅延している状況でも、GC世代の変更を検知してrunfinqを定期的にウェイクアップする重要な役割を果たします。

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

src/pkg/runtime/mgc0.cファイルにおける変更点は以下の通りです。

  1. wakefing関数のプロトタイプ宣言の追加:

    static void	wakefing(void);
    
  2. MHeap構造体にlastsweepgenフィールドの追加:

    static struct
    {
    	G*	g;
    	bool	parked;
    	uint32	lastsweepgen; // 追加
    
    	MSpan**	spans;
    	uint32	nspan;
    	// ...
    } sweep;
    
  3. bgsweep関数内の変更:

    bgsweep(void)
    {
    	for(;;) {
    		while(runtime·sweepone() != -1) {
    			gcstats.nbgsweep++;
    			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);
    		// 削除されたコードブロック: finq != nil のチェックと fing の起動/ウェイクアップ
    		if(!runtime·mheap.sweepdone) {
    			// ...
    		}
    		// ...
    	}
    }
    
  4. runtime·gc関数内の変更:

    runtime·gc(int32 force)
    {
    	// ...
    
    	// now that gc is done, kick off finalizer thread if needed
    	if(!ConcurrentSweep) {
    		// kick off goroutine to run queued finalizers
    		wakefing(); // 変更: 従来の複雑なロジックを wakefing() に置き換え
    		// give the queued finalizers, if any, a chance to run
    		runtime·gosched();
    	}
    	// ...
    }
    
  5. wakefing関数の新規追加:

    static void
    wakefing(void)
    {
    	if(finq == nil)
    		return;
    	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);
    }
    

コアとなるコードの解説

このコミットの核心は、ファイナライザ処理の起動ロジックを一元化し、より堅牢にすることです。

  • wakefing関数の導入:

    • この関数は、ファイナライザキューfinqにファイナライザが存在するかどうかを最初にチェックします。存在しない場合は何もせずに関数を終了します。
    • gclockというグローバルロックを取得し、ファイナライザ関連の共有データへのアクセスを保護します。
    • fing(ファイナライザを実行するゴルーチンへのポインタ)がnilの場合、つまりファイナライザゴルーチンがまだ起動されていない場合は、runtime·newproc1を呼び出して新しいゴルーチンを起動し、runfinqv関数(ファイナライザを実行する実際の関数)を実行させます。
    • fingnilでなく、かつfingwaittrueの場合(ファイナライザゴルーチンが現在待機状態にある場合)、fingwaitfalseに設定し、runtime·ready(fing)を呼び出してそのゴルーチンを再開可能状態にします。
    • 最後にgclockを解放します。
    • この関数により、ファイナライザゴルーチンの起動とウェイクアップのロジックがカプセル化され、複数の場所から安全に呼び出せるようになりました。
  • bgsweep関数内の変更:

    • sweep.lastsweepgenという新しいフィールドが導入され、現在のGCスイープ世代を追跡します。
    • while(runtime·sweepone() != -1)ループ内で、sweep.lastsweepgenruntime·mheap.sweepgen(現在のヒープのスイープ世代)と異なる場合、つまり新しいGCサイクルが開始されたことを意味する場合に、sweep.lastsweepgenを更新し、wakefing()を呼び出します。この変更は非常に重要です。これにより、bgsweepが前のGCサイクルのスイープに追いついていない状況でも、新しいGCサイクルが始まるたびにrunfinqがウェイクアップされる機会が与えられます。これは、ファイナライザがGCサイクルごとに少なくとも一度は処理されることを保証するための「安全弁」として機能します。
    • スイープループが終了した後(runtime·sweepone()-1を返した後)にも、wakefing()が呼び出されます。これは、スイープが完了した時点で、まだ処理されていないファイナライザがあれば確実に処理を開始させるためです。
    • 従来のfinq != nilのチェックとfingの起動/ウェイクアップに関する複雑なコードブロックが削除され、wakefing()の呼び出しに置き換えられました。これにより、コードが簡潔になり、ロジックの重複が解消されました。
  • runtime·gc関数内の変更:

    • GCが完了した後、ConcurrentSweepが有効でない場合に、従来のファイナライザ起動ロジックが単純に**wakefing()の呼び出しに置き換えられました**。これにより、GC完了後のファイナライザ処理の起動がより直接的かつ確実になります。

これらの変更により、Goランタイムは、bgsweepの進行状況やGCのタイミングに依存することなく、ファイナライザが確実に処理されるようになりました。これは、リソースリークの防止と、ファイナライザに依存するアプリケーションの安定性向上に貢献します。

関連リンク

参考にした情報源リンク