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

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

このコミットは、Goランタイムにおけるガベージコレクション(GC)の挙動に関する重要な修正を含んでいます。具体的には、g0(システムゴルーチン)がGCをトリガーしないように変更することで、worldsema(ワールドセマフォ)によるゴルーチンのパーク(一時停止)処理との競合を解消し、g0がパークできないという特性に起因する問題を解決しています。これにより、GCの安定性と正確性が向上しています。

コミット

commit dfdd1ba028b0adc78f858850a255bcd57aabef86
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Aug 22 02:17:45 2013 +0400

    runtime: do not trigger GC on g0
    GC acquires worldsema, which is a goroutine-level semaphore
    which parks goroutines. g0 can not be parked.
    Fixes #6193.
    
    R=khr, khr
    CC=golang-dev
    https://golang.org/cl/12880045

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

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

元コミット内容

このコミットは、Goランタイムのガベージコレクション(GC)がg0ゴルーチン上でトリガーされないようにする変更です。GCはworldsemaというゴルーチンレベルのセマフォを取得し、これによって他のゴルーチンをパーク(一時停止)させます。しかし、g0ゴルーチンはパークすることができないため、g0上でGCがトリガーされると問題が発生していました。このコミットは、この問題を修正し、Issue #6193を解決します。

変更の背景

Goランタイムのガベージコレクションは、ヒープの一貫性を保つために「Stop-the-World (STW)」フェーズを伴います。このSTWフェーズ中、すべてのユーザーゴルーチンは一時的に停止され、GCが安全にヒープをスキャンし、マークする作業を行います。このSTWの調整にはworldsemaというセマフォが使用されます。

g0は、Goランタイムにおいて非常に特殊なゴルーチンです。通常のユーザーゴルーチンがGoのコードを実行するのに対し、g0は各OSスレッド(M、またはマシン)に関連付けられたシステムゴルーチンであり、GC、スケジューラ操作、システムコールハンドリングなど、ランタイム内部のコードを実行する役割を担っています。STWフェーズ中、g0はMスタック上でアクティブなままGC作業を実行します。

問題は、GCがworldsemaを取得してゴルーチンをパークしようとする際に発生しました。g0はランタイムの根幹を担うゴルーチンであり、その性質上、パークされることを想定していません。もしg0上でGCがトリガーされ、g0自身がworldsemaを介してパークされようとすると、デッドロックやランタイムのクラッシュといった予期せぬ挙動を引き起こす可能性がありました。

このコミットは、このようなg0の特性とGCの動作の間の不整合を解消するために導入されました。具体的には、g0上でGCが実行されることを防ぐことで、worldsemaによるパーク処理がg0に適用されることを回避し、ランタイムの安定性を確保することを目的としています。コミットメッセージに「Fixes #6193」とあることから、この問題は以前から認識されており、このコミットでその解決が図られたことがわかります。

前提知識の解説

このコミットを理解するためには、Goランタイムの以下の主要な概念を理解しておく必要があります。

  1. ゴルーチン (Goroutine): Goにおける軽量な実行スレッドです。OSスレッドよりもはるかに軽量で、数千から数百万のゴルーチンを同時に実行できます。Goランタイムのスケジューラによって管理され、OSスレッドに多重化されて実行されます。

  2. M (Machine) と P (Processor): Goランタイムのスケジューラにおける重要な要素です。

    • M (Machine): OSスレッドを表します。Goプログラムは複数のMを生成し、それぞれがOSスレッドとして動作します。
    • P (Processor): 論理プロセッサを表します。Mがゴルーチンを実行するためのコンテキストを提供します。Pは実行可能なゴルーチンキューを持ち、MはPからゴルーチンを取得して実行します。
  3. g0 (System Goroutine): 各M(OSスレッド)に紐付けられた特別なゴルーチンです。ユーザーゴルーチンとは異なり、Goランタイムの内部処理(スケジューリング、GC、スタック管理、システムコールハンドリングなど)を実行するために使用されます。g0はユーザーゴルーチンのスタックとは異なる、固定サイズのスタックを持ち、Goランタイムのコア部分を担います。g0は決してパーク(一時停止)されるべきではありません。

  4. ガベージコレクション (Garbage Collection, GC): Goのメモリ管理システムの中核をなす機能です。不要になったメモリ領域を自動的に解放し、メモリリークを防ぎます。GoのGCは並行GCであり、ほとんどの作業はユーザーゴルーチンと並行して実行されますが、ヒープの一貫性を確保するために「Stop-the-World (STW)」フェーズが存在します。

  5. Stop-the-World (STW): GCサイクルの一部で、すべてのユーザーゴルーチンが一時的に実行を停止するフェーズです。この間、GCはヒープのスキャンやマークといった重要な作業を、ヒープが変更されない安全な状態で実行できます。STWの時間はGoアプリケーションのパフォーマンスに大きな影響を与えるため、GoランタイムはSTW時間を最小限に抑えるように設計されています。

  6. worldsema (World Semaphore): GoランタイムがSTWフェーズを開始する際に使用するセマフォです。GCやその他のランタイム操作がすべてのユーザーゴルーチンを一時停止する必要がある場合、このセマフォを取得します。このセマフォが取得されると、他のゴルーチンは安全なポイントで実行を停止するようにシグナルを受け取ります。g0worldsemaによってパークされるべきではありません。

これらの概念を理解することで、g0がパークできないという特性と、GCがworldsemaを使用するという事実が、どのように問題を引き起こし、このコミットがそれをどのように解決しているのかが明確になります。

技術的詳細

このコミットの核心は、g0ゴルーチンがガベージコレクション(GC)をトリガーしないようにすることです。これは、GCがworldsemaというセマフォを使用してゴルーチンをパーク(一時停止)させるのに対し、g0はパークできないというGoランタイムの設計上の制約に起因する問題を解決するためです。

変更は主にsrc/pkg/runtime/mgc0.csrc/pkg/runtime/stack.cの2つのファイルで行われています。

src/pkg/runtime/mgc0.cの変更点

このファイルはGoランタイムのGCの主要なロジックを含んでいます。

  1. addroots関数の変更: addroots関数は、GCのマークフェーズ中にルートオブジェクト(GCの起点となるオブジェクト)を追加する役割を担っています。以前のコードでは、Grunning状態のゴルーチン(現在実行中のゴルーチン)に対して、いくつかのチェックを行っていました。

    // 変更前
    case Grunning:
    	if(gp != m->curg)
    		runtime·throw("mark - world not stopped");
    	if(g != m->g0)
    		runtime·throw("gc not on g0");
    	addstackroots(gp);
    	break;
    

    この部分では、GCがg0上で実行されていることを前提としていました。しかし、g0がパークできないという問題があるため、この前提は削除されました。

    // 変更後
    case Grunning:
    	runtime·throw("mark - world not stopped");
    

    変更後では、Grunning状態のゴルーチンがGC中に存在する場合、それは「world not stopped」というエラーとして扱われるようになりました。これは、GCが実行されている間はすべてのユーザーゴルーチンが停止しているべきであり、Grunning状態のゴルーチンが存在すること自体が問題であるという、より厳密なチェックになったことを示唆しています。g0上でのGC実行の前提がなくなったため、g0に関するチェックも削除されました。

  2. runtime·gc関数の変更: runtime·gc関数は、GCのメインエントリポイントです。

    • GCトリガー条件の変更: GCがトリガーされる条件をチェックする箇所に、g == m->g0という条件が追加されました。

      // 変更前
      if(!mstats.enablegc || m->locks > 0 || runtime·panicking)
      	return;
      // 変更後
      if(!mstats.enablegc || g == m->g0 || m->locks > 0 || runtime·panicking)
      	return;
      

      これにより、現在実行中のゴルーチンgg0である場合、GCはトリガーされずに即座にリターンするようになりました。これは、g0上でGCが実行されることを明示的に禁止する最も重要な変更点です。

    • GC実行ロジックの変更: GCを実行する際に、g0への切り替えロジックが簡素化されました。

      // 変更前
      for(i = 0; i < (runtime·debug.gctrace > 1 ? 2 : 1); i++) {
      	if(g == m->g0) {
      		// already on g0
      		gc(&a);
      	} else {
      		// switch to g0, call gc(&a), then switch back
      		g->param = &a;
      		g->status = Gwaiting;
      		g->waitreason = "garbage collection";
      		runtime·mcall(mgc);
      	}
      	// record a new start time in case we're going around again
      	a.start_time = runtime·nanotime();
      }
      // 変更後
      for(i = 0; i < (runtime·debug.gctrace > 1 ? 2 : 1); i++) {
      	// switch to g0, call gc(&a), then switch back
      	g->param = &a;
      	g->status = Gwaiting;
      	g->waitreason = "garbage collection";
      	runtime·mcall(mgc);
      	// record a new start time in case we're going around again
      	a.start_time = runtime·nanotime();
      }
      

      変更前は、もし現在のゴルーチンが既にg0であれば直接gc(&a)を呼び出し、そうでなければg0に切り替えてからmgcを呼び出すという条件分岐がありました。変更後では、g0上でGCがトリガーされないようになったため、常にg0に切り替えてからGCを実行するロジックに統一されました。これは、GCがユーザーゴルーチンから呼び出された場合にのみ、g0に切り替えてGCを実行するという意図を反映しています。

    • 後処理の変更: GC後の後処理からもg0に関する条件分岐が削除されました。

      // 変更前
      if(g->preempt)  // restore the preemption request in case we've cleared it in newstack
      	g->stackguard0 = StackPreempt;
      // give the queued finalizers, if any, a chance to run
      if(g != m->g0)
      	runtime·gosched();
      // 変更後
      // give the queued finalizers, if any, a chance to run
      runtime·gosched();
      

      g->preemptに関する行は、このコミットとは直接関係ない変更のようです(おそらく別のコミットで削除されたか、このコミットで不要になったため削除された)。重要なのは、g != m->g0という条件が削除され、常にruntime·gosched()が呼び出されるようになった点です。これは、GCがg0上で直接実行されることがなくなったため、ユーザーゴルーチンがGC後にスケジューリングされることを保証するための変更です。

src/pkg/runtime/stack.cの変更点

このファイルはGoランタイムのスタック管理に関連する関数を含んでいます。

  1. runtime·stackalloc関数の変更: スタックを割り当てるruntime·stackalloc関数において、runtime·mallocgcのフラグにFlagNoInvokeGCが追加されました。
    // 変更前
    return runtime·mallocgc(n, 0, FlagNoProfiling|FlagNoGC|FlagNoZero);
    // 変更後
    return runtime·mallocgc(n, 0, FlagNoProfiling|FlagNoGC|FlagNoZero|FlagNoInvokeGC);
    
    FlagNoInvokeGCは、このメモリ割り当てがGCをトリガーしないことを示すフラグです。スタックの割り当てはランタイムの非常に低レベルな操作であり、この操作中にGCがトリガーされると、さらなる複雑性やデッドロックを引き起こす可能性があります。特に、g0がスタックを割り当てる際にGCがトリガーされることを防ぐために、このフラグが追加されたと考えられます。これにより、スタック割り当ての安定性が向上します。

これらの変更は、g0がパークできないという根本的な制約を尊重し、GCのロジックをg0の特殊な性質に合わせて調整することで、ランタイムの堅牢性を高めることを目的としています。

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

src/pkg/runtime/mgc0.c

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1614,12 +1614,7 @@ addroots(void)
 		case Gdead:
 			break;
 		case Grunning:
-			if(gp != m->curg)
-				runtime·throw("mark - world not stopped");
-			if(g != m->g0)
-				runtime·throw("gc not on g0");
-			addstackroots(gp);
-			break;
+			runtime·throw("mark - world not stopped");
 		case Grunnable:
 		case Gsyscall:
 		case Gwaiting:
@@ -2046,7 +2041,7 @@ runtime·gc(int32 force)
 	// problems, don't bother trying to run gc
 	// while holding a lock.  The next mallocgc
 	// without a lock will do the gc instead.
-	if(!mstats.enablegc || m->locks > 0 || runtime·panicking)
+	if(!mstats.enablegc || g == m->g0 || m->locks > 0 || runtime·panicking)
 		return;
 
 	if(gcpercent == GcpercentUnknown) {	// first time through
@@ -2077,16 +2072,11 @@ runtime·gc(int32 force)
 	// we don't need to scan gc's internal state).  Also an
 	// enabler for copyable stacks.
 	for(i = 0; i < (runtime·debug.gctrace > 1 ? 2 : 1); i++) {
-		if(g == m->g0) {
-			// already on g0
-			gc(&a);
-		} else {
-			// switch to g0, call gc(&a), then switch back
-			g->param = &a;
-			g->status = Gwaiting;
-			g->waitreason = "garbage collection";
-			runtime·mcall(mgc);
-		}
+		// switch to g0, call gc(&a), then switch back
+		g->param = &a;
+		g->status = Gwaiting;
+		g->waitreason = "garbage collection";
+		runtime·mcall(mgc);
 		// record a new start time in case we're going around again
 		a.start_time = runtime·nanotime();
 	}
@@ -2110,11 +2100,8 @@ runtime·gc(int32 force)
 		}
 		runtime·unlock(&finlock);
 	}
-	if(g->preempt)  // restore the preemption request in case we've cleared it in newstack
-		g->stackguard0 = StackPreempt;
 	// give the queued finalizers, if any, a chance to run
-	if(g != m->g0)
-		runtime·gosched();
+	runtime·gosched();
 }
 
 static void

src/pkg/runtime/stack.c

--- a/src/pkg/runtime/stack.c
+++ b/src/pkg/runtime/stack.c
@@ -105,7 +105,7 @@ runtime·stackalloc(uint32 n)
 		m->stackinuse++;
 		return v;
 	}
-	return runtime·mallocgc(n, 0, FlagNoProfiling|FlagNoGC|FlagNoZero);
+	return runtime·mallocgc(n, 0, FlagNoProfiling|FlagNoGC|FlagNoZero|FlagNoInvokeGC);
 }
 
 void

コアとなるコードの解説

src/pkg/runtime/mgc0.c

  1. addroots関数の変更:

    • 変更前: Grunning状態のゴルーチンがm->curg(現在のMで実行中のゴルーチン)と一致し、かつg0でない場合にaddstackroots(gp)を呼び出していました。これは、GCがg0上で実行されていることを前提とし、ユーザーゴルーチンのスタックルートをスキャンするロジックでした。
    • 変更後: Grunning状態のゴルーチンが見つかった場合、無条件にruntime·throw("mark - world not stopped")を呼び出すようになりました。これは、GCのSTWフェーズ中にユーザーゴルーチンがGrunning状態であること自体が異常であるという、より厳密なチェックを導入したことを意味します。g0上でGCが実行されないという新しい前提により、g0に関する条件分岐は不要になりました。
  2. runtime·gc関数の変更:

    • GCトリガー条件の変更: if(!mstats.enablegc || m->locks > 0 || runtime·panicking) の条件に g == m->g0 が追加されました。 これは、GCをトリガーしようとしている現在のゴルーチンがg0である場合、GCの実行を即座に中断(return)することを意味します。これにより、g0がGCを直接トリガーすることを防ぎ、worldsemaによるパークの問題を回避します。

    • GC実行ロジックの変更: GCの実際の処理を行う部分で、g == m->g0の条件分岐が削除されました。 変更前は、もし現在のゴルーチンが既にg0であれば直接gc(&a)を呼び出し、そうでなければg0に切り替えてからruntime·mcall(mgc)を呼び出していました。 変更後では、g0上でGCがトリガーされないという新しい前提があるため、常にユーザーゴルーチンからg0に切り替えてruntime·mcall(mgc)を呼び出すロジックに統一されました。runtime·mcallは、現在のゴルーチンを一時停止し、g0に切り替えて指定された関数(この場合はmgc)を実行するためのGoランタイムの内部関数です。

    • 後処理の変更: if(g != m->g0)という条件分岐が削除され、常にruntime·gosched()が呼び出されるようになりました。 runtime·gosched()は、現在のゴルーチンを一時停止し、スケジューラに制御を戻して他のゴルーチンが実行される機会を与える関数です。GCがg0上で直接実行されないようになったため、GC完了後にユーザーゴルーチンが適切にスケジューリングされることを保証するために、無条件でruntime·gosched()を呼び出すようになりました。

src/pkg/runtime/stack.c

  1. runtime·stackalloc関数の変更: runtime·mallocgc関数に渡されるフラグにFlagNoInvokeGCが追加されました。 runtime·mallocgcは、Goランタイムがメモリを割り当てる際に使用する関数で、必要に応じてGCをトリガーする可能性があります。 FlagNoInvokeGCは、このメモリ割り当てがGCをトリガーすべきではないことを示すフラグです。スタックの割り当ては非常に低レベルな操作であり、この操作中にGCがトリガーされると、ランタイムのデッドロックや不安定性を引き起こす可能性があります。特に、g0がスタックを割り当てる際にGCがトリガーされることを防ぐことで、ランタイムの安定性を向上させています。

これらの変更は全体として、g0の特殊な性質(パークできない)を尊重し、GCのロジックがg0に不適切な影響を与えないようにするためのものです。これにより、GoランタイムのGCの堅牢性と正確性が向上しています。

関連リンク

参考にした情報源リンク

  • Goのガベージコレクションに関する公式ドキュメントやブログ記事 (一般的なGo GCの仕組みについて)
  • Goランタイムのソースコード (src/runtime ディレクトリ)
  • Goのスケジューラに関する解説記事 (M, P, Gの概念について)
  • Goのg0ゴルーチンに関する技術ブログやドキュメント (g0の役割と特性について)
  • Web検索結果: "Go runtime GC on g0 worldsema issue #6193" (Goのg0worldsema、STWに関する一般的な情報)