[インデックス 18259] ファイルの概要
このコミットは、Goランタイムのガベージコレクション(GC)プロセスにおけるデータ競合を修正するものです。具体的には、GCヘルパーが並行して動作する際に、work.ndone
カウンタの更新とwork.nproc
の値の参照が同時に行われることで発生する競合状態を解消します。この修正により、GCの安定性と正確性が向上します。
コミット
b3a3afc9b788597ead21ea4770c9679f31475f40
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b3a3afc9b788597ead21ea4770c9679f31475f40
元コミット内容
commit b3a3afc9b788597ead21ea4770c9679f31475f40
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Jan 15 19:38:08 2014 +0400
runtime: fix data race in GC
Fixes #5139.
Update #7065.
R=golang-codereviews, bradfitz, minux.ma
CC=golang-codereviews
https://golang.org/cl/52090045
変更の背景
Goのガベージコレクションは、複数のGCヘルパーゴルーチンが並行して動作することで効率的にメモリを管理します。これらのヘルパーは、GC作業の完了をwork.ndone
という共有カウンタをインクリメントすることで報告します。そして、全てのヘルパーが作業を完了したかどうかをwork.nproc
(GCに参加しているプロセッサ/ヘルパーの総数)と比較することで判断します。
このコミットが修正する問題は、work.ndone
がインクリメントされた直後にwork.nproc
の値が変更される可能性があるというデータ競合です。もしwork.nproc
が、runtime·xadd(&work.ndone, +1)
が実行された直後、かつwork.nproc-1
との比較が行われる前に変更された場合、work.ndone
とwork.nproc
の比較が不正な状態で行われる可能性があります。これにより、GCの完了条件が正しく評価されず、GCプロセスがハングアップしたり、不正確な動作を引き起こしたりする可能性がありました。
具体的には、runtime·xadd(&work.ndone, +1) == work.nproc-1
という行で、work.ndone
のインクリメントはアトミックに行われますが、その直後にwork.nproc
が別のゴルーチンによって変更されると、比較に使用されるwork.nproc
の値が、work.ndone
がインクリメントされた時点での期待される値と異なる可能性があります。これは典型的なデータ競合のシナリオであり、並行プログラミングにおける共有状態の不適切なアクセスによって引き起こされます。
前提知識の解説
- ガベージコレクション (GC): プログラムが動的に確保したメモリのうち、もはや使用されていない領域を自動的に解放するプロセスです。GoのGCは並行(concurrent)かつ並列(parallel)に動作し、プログラムの実行と同時にGC処理を進めることで、アプリケーションの一時停止時間(STW: Stop The World)を最小限に抑える設計になっています。
- データ競合 (Data Race): 複数のゴルーチン(またはスレッド)が同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって順序付けされていない場合に発生するプログラミングエラーです。データ競合は予測不能な動作、プログラムのクラッシュ、またはデータの破損を引き起こす可能性があります。Goには、
go run -race
コマンドでデータ競合を検出する組み込みのレース検出器があります。 - アトミック操作 (Atomic Operations): 複数の操作が不可分(アトミック)に実行されることを保証する操作です。つまり、その操作が完了するまで、他のゴルーチンはその操作の途中の状態を観測したり、干渉したりすることはできません。
runtime·xadd
のような関数は、アトミックな加算操作を提供し、共有カウンタの安全なインクリメントを可能にします。 runtime·xadd
: Goランタイム内部で使用されるアトミックな加算関数です。指定されたメモリ位置の値をアトミックにインクリメントし、インクリメント後の値を返します。これにより、複数のゴルーチンが同時にカウンタを更新しても、競合状態が発生しないようにします。
技術的詳細
このデータ競合は、runtime·gchelper
関数内で発生していました。この関数はGCヘルパーゴルーチンによって実行され、GC作業の一部を担います。GCヘルパーは、割り当てられた作業を完了した後、work.ndone
カウンタをインクリメントし、全てのヘルパーが完了したかどうかをチェックします。
元のコードでは、以下のようになっていました。
if(runtime·xadd(&work.ndone, +1) == work.nproc-1)
runtime·notewakeup(&work.alldone);
ここで問題となるのは、runtime·xadd(&work.ndone, +1)
が実行され、work.ndone
がアトミックにインクリメントされた後、work.nproc-1
との比較が行われるまでの間に、別のゴルーチンがwork.nproc
の値を変更する可能性がある点です。例えば、GCのフェーズが変わり、GCヘルパーの数が動的に調整されるようなシナリオが考えられます。
もしwork.nproc
が減少した場合、work.ndone
が期待される値に達しても、work.nproc-1
がそれよりも小さい値になってしまい、work.ndone == work.nproc-1
の条件が永遠に満たされない可能性があります。これにより、GCの完了を待つゴルーチンがデッドロックに陥るか、GCが正常に終了しないという問題が発生します。
この修正は、work.nproc
の値をruntime·xadd
の呼び出し前にローカル変数nproc
にコピーすることで、この競合を解消します。
int32 nproc;
// ...
nproc = work.nproc; // work.nproc can change right after we increment work.ndone
if(runtime·xadd(&work.ndone, +1) == nproc-1)
runtime·notewakeup(&work.alldone);
これにより、runtime·xadd
が実行された後にwork.nproc
が変更されたとしても、比較に使用されるnproc
の値は、runtime·xadd
が実行される直前のwork.nproc
の値を保持しているため、一貫性が保たれます。これは、共有変数へのアクセスを最小限に抑え、スナップショットを取ることでデータ競合を回避する一般的な手法です。
コアとなるコードの変更箇所
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1956,6 +1956,8 @@ runtime·memorydump(void)
void
runtime·gchelper(void)
{
+ int32 nproc;
+
gchelperstart();
// parallel mark for over gc roots
@@ -1972,7 +1974,8 @@ runtime·gchelper(void)
runtime·parfordo(work.sweepfor);
bufferList[m->helpgc].busy = 0;
- if(runtime·xadd(&work.ndone, +1) == work.nproc-1)
+ nproc = work.nproc; // work.nproc can change right after we increment work.ndone
+ if(runtime·xadd(&work.ndone, +1) == nproc-1)
runtime·notewakeup(&work.alldone);
}
コアとなるコードの解説
変更はsrc/pkg/runtime/mgc0.c
ファイルのruntime·gchelper
関数内で行われています。
-
int32 nproc;
の追加:runtime·gchelper
関数の冒頭に、int32
型のローカル変数nproc
が宣言されました。これは、work.nproc
の値を一時的に保持するための変数です。 -
nproc = work.nproc;
の追加:runtime·xadd(&work.ndone, +1)
の呼び出しの直前に、work.nproc
の現在の値がnproc
に代入されます。この行のコメント「// work.nproc can change right after we increment work.ndone
」が、この変更の意図を明確に示しています。つまり、work.ndone
をインクリメントする直前のwork.nproc
の値を「スナップショット」として取得しているわけです。 -
比較対象の変更:
if
文の条件式がif(runtime·xadd(&work.ndone, +1) == work.nproc-1)
からif(runtime·xadd(&work.ndone, +1) == nproc-1)
に変更されました。これにより、work.ndone
のインクリメント後の値と、ローカル変数nproc
に保存されたwork.nproc
の「スナップショット」値が比較されるようになります。
この修正により、work.ndone
がアトミックに更新された後でwork.nproc
が別のゴルーチンによって変更されたとしても、比較は一貫性のあるnproc
の値に対して行われるため、データ競合が解消され、GCの完了条件が正しく評価されるようになります。
関連リンク
- Fixes #5139. (Go issue 5139)
- Update #7065. (Go issue 7065)
- https://golang.org/cl/52090045 (Gerrit Code Review for this change)
参考にした情報源リンク
- Goのデータ競合に関する一般的な情報:
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHkIO85_Vy5hLGLNrfiWZ0FZDUxt00Uv6gjWg62vEK3CUWkER0y_uDTb7cU4E3dYk9EDzQobsprilCIRfnNWn8stD2JjsZ7S1W1VRhDi0rUORX1SYmnfwBDipnTiQLHm6ltNZYNYKcr2rdGo3lgl22UtCTiGK4RKHkkf0Zkmnrg1JQ6WrNe0Ve_
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFxRhNpECsOo2JRuzNHkHcl8Y0s2tIaSY9aIi-N_Mb3Ko1hTZCoQy1Y7hV_oPW8xgqdFLiNSYtVTL7-Pppq1kjELMb7GoXiaGc2evBeRbRsERxGrwLAxcE_ZnckXgGXYzPerpk5
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEgINdRwuPylDTmhn2YtjYyFZHeln_0vZdwDJtdji6ru2r5d7nGHDGW-fE3CYzCmyhljveWFJVrHLx8aaVP9pukbdLlubi7M8_bzZ36JH-O7uRD1jzwb0B8IYqwq8_z7rXrGLd9jxZGrhBlilFF-fulbWEj8sb6MZutQlrW7noaDK2tnp8483rInxp4bEayzuaAlIUbf5o4
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF95-nzUhKca3KuMCzOR-XRbNGdl6vE0AL0ARl-hOiXgcxlX-ltlvd75vIAEsBffsLSk-xCS7OxzWi8eR33bHaeBt83O8FMIZn-D_R1z415oyKxo3o8tcWEXYGWsyV-5U75OYYJkz0=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHCJQUP6K-KR4Q-OiqzosNxFD3yLLhazntKTsydJ0Ajap2JP-9wTS4MvH7cjWiJDSdL9prtJGDU9Yl6XS3JY8Yc4cHZEH0IXanp2aY04MPuzbYcKUjcWwFiZk6knuXQcEUsh8fFKgHi2tDb5Dx3w0UIJBVG7G2dRb0qF9cJZq-FEiR2oq3QCf9yvU=
- GoのGCに関する一般的な情報: