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

[インデックス 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.ndonework.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関数内で行われています。

  1. int32 nproc; の追加: runtime·gchelper関数の冒頭に、int32型のローカル変数nprocが宣言されました。これは、work.nprocの値を一時的に保持するための変数です。

  2. 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の値を「スナップショット」として取得しているわけです。

  3. 比較対象の変更: 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)

参考にした情報源リンク