[インデックス 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ランタイムでは、gcpercent
がGcpercentUnknown
(未初期化状態を示す特別な値)である場合、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·lock
とruntime·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
の値を変更しようとすると、以下のシナリオで競合状態が発生する可能性がありました。
- ゴルーチンAが
runtime·gc
関数に入り、gcpercent == GcpercentUnknown
を検出。 - ゴルーチンBが
setGCPercent
を呼び出し、gcpercent
を例えば200に設定。 - ゴルーチンAが
readgogc()
を実行し、環境変数GOGC
が設定されていない場合、デフォルト値の100を読み込む。 - ゴルーチン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);
}
この変更により、以下のようになります。
- ゴルーチンAが
runtime·gc
関数に入り、gcpercent == GcpercentUnknown
を検出。 - ゴルーチンAが
runtime·lock(&runtime·mheap)
を呼び出し、runtime·mheap
のロックを取得。 - この時点で、他のゴルーチン(例えばゴルーチンB)が
setGCPercent
を呼び出しても、runtime·mheap
のロックが取得できないため、setGCPercent
はブロックされます。 - ゴルーチンAは再度
if(gcpercent == GcpercentUnknown)
をチェックします。これは、ロックを取得するまでの間に別のゴルーチンがgcpercent
を初期化している可能性を考慮するためです(ただし、この特定のケースでは、ロックが取得される前にgcpercent
が変更されることはありませんが、一般的なロックパターンとして二重チェックが用いられます)。 - ゴルーチンAが
gcpercent = readgogc();
を実行し、gcpercent
を安全に初期化します。 - ゴルーチンAが
runtime·unlock(&runtime·mheap)
を呼び出し、ロックを解放します。 - これで、ブロックされていたゴルーチン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
関数内で行われています。
元のコードでは、gcpercent
がGcpercentUnknown
(初期状態)の場合に、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のガベージコレクションに関する一般的なドキュメントと解説
- 並行処理と競合状態に関する一般的な情報
- ミューテックスとロックの概念に関する情報