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

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

このコミットは、Goランタイムにおけるガベージコレクション(GC)とsetGCPercent関数の間の競合状態(race condition)を修正するものです。具体的には、GCが初めて実行される際にgcpercentの値を読み込む処理と、setGCPercentによるgcpercentの値の更新が同時に行われた場合に、gcpercentがデフォルト値で上書きされてしまう問題を解決します。

コミット

commit 94dc963b558b3d37906af53eca45c5ae807a9e84
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Sat Jun 15 16:07:06 2013 +0400

    runtime: fix race condition between GC and setGCPercent
    If first GC runs concurrently with setGCPercent,
    it can overwrite gcpercent value with default.
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/10242047

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

https://github.com/golang/go/commit/94dc963b558b3d37906af53eca45c5ae807a9e84

元コミット内容

runtime: fix race condition between GC and setGCPercent
If first GC runs concurrently with setGCPercent,
it can overwrite gcpercent value with default.

変更の背景

Goランタイムのガベージコレクタは、プログラムのメモリ管理を自動的に行います。gcpercentは、Goのガベージコレクタがどれくらいの頻度で実行されるかを制御する重要なパラメータです。この値は、前回のGC後にヒープサイズがどれだけ増加したら次のGCをトリガーするかをパーセンテージで指定します。例えば、gcpercentが100の場合、ヒープサイズが前回のGC後の2倍になったときにGCが実行されます。

このコミットが修正しようとしている問題は、Goプログラムの起動時、またはGCが初めて実行される際に発生する可能性のある競合状態です。具体的には、runtime·gc関数が初めてgcpercentの値を初期化しようとするタイミングと、ユーザーや他のランタイムコンポーネントがsetGCPercent関数を通じてgcpercentの値を設定しようとするタイミングが重なった場合に問題が生じます。

初期のGoランタイムでは、gcpercentGcpercentUnknown(未初期化状態を示す特別な値)である場合、readgogc()関数を呼び出して環境変数GOGCから値を読み込むか、デフォルト値を使用するロジックがありました。この処理がロックなしで行われていたため、もしsetGCPercentが同時にgcpercentを更新しようとすると、readgogc()が読み込んだデフォルト値や環境変数からの値が、setGCPercentによって設定された意図された値を上書きしてしまう可能性がありました。これにより、ユーザーが設定したGCの挙動が無視され、予期せぬパフォーマンス特性やメモリ使用量につながる可能性がありました。

前提知識の解説

  • ガベージコレクション (GC): プログラムが動的に確保したメモリのうち、もはや使用されなくなった領域を自動的に解放するプロセスです。GoのGCは並行(concurrent)かつ低遅延(low-latency)なマーク&スイープ方式を採用しています。
  • gcpercent: Goランタイムのガベージコレクタの動作を制御するパラメータの一つです。これは、前回のGCが完了した時点のライブヒープサイズに対して、ヒープがどれだけ増加したら次のGCサイクルを開始するかをパーセンテージで指定します。デフォルト値は100で、これはライブヒープサイズが2倍になったらGCを実行することを意味します。この値を調整することで、GCの頻度とアプリケーションのメモリ使用量のバランスを調整できます。
  • 競合状態 (Race Condition): 複数の並行に動作するプロセスやスレッドが共有リソース(この場合はgcpercent変数)にアクセスし、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。競合状態は、プログラムのバグの一般的な原因であり、デバッグが困難な場合があります。
  • ミューテックス (Mutex) とロック: 共有リソースへのアクセスを同期するためのメカニズムです。ミューテックスは「相互排他」を意味し、一度に一つのスレッドだけが特定のコードセクション(クリティカルセクション)を実行できるようにします。runtime·lockruntime·unlockは、Goランタイム内部で使用される低レベルのロックプリミティブです。これらを使用することで、複数のゴルーチンが同時にgcpercentのような共有変数にアクセスする際に、一貫性を保つことができます。
  • runtime·mheap: Goランタイムのヒープ(メモリ領域)全体を管理する構造体です。この構造体には、ヒープの状態に関する情報や、ヒープ操作を保護するためのロックが含まれています。runtime·mheapに対するロックは、ヒープの整合性を保つために非常に重要です。

技術的詳細

このコミットは、src/pkg/runtime/mgc0.cファイルのruntime·gc関数内のgcpercentの初期化ロジックにロックを追加することで、競合状態を解消しています。

修正前のコードでは、gcpercent == GcpercentUnknown(GCが初めて実行されることを示す)の場合に、gcpercent = readgogc();という行がロックなしで実行されていました。このとき、もし別のゴルーチンがruntime.SetGCPercent()(内部的にはsetGCPercentを呼び出す)を呼び出してgcpercentの値を変更しようとすると、以下のシナリオで競合状態が発生する可能性がありました。

  1. ゴルーチンAがruntime·gc関数に入り、gcpercent == GcpercentUnknownを検出。
  2. ゴルーチンBがsetGCPercentを呼び出し、gcpercentを例えば200に設定。
  3. ゴルーチンAがreadgogc()を実行し、環境変数GOGCが設定されていない場合、デフォルト値の100を読み込む。
  4. ゴルーチンAがgcpercent = 100;を実行し、ゴルーチンBが設定した200を上書きしてしまう。

この競合状態を解決するために、コミットではgcpercentの初期化処理をruntime·lock(&runtime·mheap)runtime·unlock(&runtime·mheap)で囲んでいます。

if(gcpercent == GcpercentUnknown) {    // first time through
    runtime·lock(&runtime·mheap);
    if(gcpercent == GcpercentUnknown)
        gcpercent = readgogc();
    runtime·unlock(&runtime·mheap);
}

この変更により、以下のようになります。

  1. ゴルーチンAがruntime·gc関数に入り、gcpercent == GcpercentUnknownを検出。
  2. ゴルーチンAがruntime·lock(&runtime·mheap)を呼び出し、runtime·mheapのロックを取得。
  3. この時点で、他のゴルーチン(例えばゴルーチンB)がsetGCPercentを呼び出しても、runtime·mheapのロックが取得できないため、setGCPercentはブロックされます。
  4. ゴルーチンAは再度if(gcpercent == GcpercentUnknown)をチェックします。これは、ロックを取得するまでの間に別のゴルーチンがgcpercentを初期化している可能性を考慮するためです(ただし、この特定のケースでは、ロックが取得される前にgcpercentが変更されることはありませんが、一般的なロックパターンとして二重チェックが用いられます)。
  5. ゴルーチンAがgcpercent = readgogc();を実行し、gcpercentを安全に初期化します。
  6. ゴルーチンAがruntime·unlock(&runtime·mheap)を呼び出し、ロックを解放します。
  7. これで、ブロックされていたゴルーチンBがsetGCPercentの処理を続行し、gcpercentを意図した値に設定できるようになります。

このように、runtime·mheapのロックを使用することで、gcpercentの初期化と変更が同時に行われることを防ぎ、データの一貫性を保証しています。

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

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1974,7 +1974,10 @@ runtime·gc(int32 force)
 		return;
 
 	if(gcpercent == GcpercentUnknown) {	// first time through
-		gcpercent = readgogc();
+		runtime·lock(&runtime·mheap);
+		if(gcpercent == GcpercentUnknown)
+			gcpercent = readgogc();
+		runtime·unlock(&runtime·mheap);
 
 		p = runtime·getenv("GOGCTRACE");
 		if(p != nil)

コアとなるコードの解説

変更はsrc/pkg/runtime/mgc0.cファイルのruntime·gc関数内で行われています。

元のコードでは、gcpercentGcpercentUnknown(初期状態)の場合に、gcpercent = readgogc();という行が直接実行されていました。

修正後のコードでは、この初期化処理がruntime·lock(&runtime·mheap)runtime·unlock(&runtime·mheap)の間に移動され、さらに内部でif(gcpercent == GcpercentUnknown)という二重チェックが追加されています。

  • runtime·lock(&runtime·mheap);: runtime·mheap構造体に関連付けられたミューテックスをロックします。これにより、他のゴルーチンが同時にruntime·mheapにアクセスしたり、gcpercentのような共有変数を変更したりするのを防ぎます。
  • if(gcpercent == GcpercentUnknown): ロックを取得した後、再度gcpercentがまだ初期化されていないかを確認します。これは、ロックを取得するまでの間に、別のゴルーチンが既にgcpercentを初期化している可能性(この特定のケースでは低いですが、一般的な並行処理のパターンとして重要)を考慮するための防御的なチェックです。
  • gcpercent = readgogc();: gcpercentがまだ初期化されていない場合、readgogc()関数を呼び出して、環境変数GOGCから値を取得するか、デフォルト値(通常は100)を設定します。この処理はロックによって保護されているため、安全に実行されます。
  • runtime·unlock(&runtime·mheap);: runtime·mheapのロックを解放します。これにより、他のゴルーチンがruntime·mheapにアクセスできるようになります。

この変更により、gcpercentの初期化がアトミック(不可分)な操作となり、setGCPercentとの間の競合状態が完全に解消されます。

関連リンク

参考にした情報源リンク

  • Goのソースコード (src/pkg/runtime/mgc0.c)
  • Goのガベージコレクションに関する一般的なドキュメントと解説
  • 並行処理と競合状態に関する一般的な情報
  • ミューテックスとロックの概念に関する情報