[インデックス 1297] ファイルの概要
このコミットは、Goランタイムのスケジューラにおける同期メカニズムの修正に関するものです。具体的には、src/runtime/proc.c
ファイル内の Note
型の同期プリミティブ sched.stopped
の使用方法を改善し、stoptheworld
処理の正確性を保証することを目的としています。
コミット
commit be629138ab5a81ccfbeeebb4ca942ac08d873820
Author: Russ Cox <rsc@golang.org>
Date: Mon Dec 8 17:14:08 2008 -0800
use Note sched.stopped correctly
R=r
DELTA=6 (5 added, 0 deleted, 1 changed)
OCL=20777
CL=20779
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/be629138ab5a81ccfbeeebb4ca942ac08d873820
元コミット内容
use Note sched.stopped correctly
このコミットは、Goランタイムのスケジューラが使用する Note
型の同期変数 sched.stopped
の利用方法を修正し、より正確な動作を保証することを目的としています。
変更の背景
Goランタイムのスケジューラは、ガベージコレクション(GC)などの特定の操作を実行する際に、実行中のすべてのゴルーチンを一時停止させる「Stop The World (STW)」というメカニズムを使用します。このSTW処理では、スケジューラがすべてのゴルーチンが停止するのを待機し、停止が完了したことを sched.stopped
という Note
を通じて通知します。
しかし、元の実装では、sched.stopped
の notewakeup
(シグナル) が、stoptheworld
が実際に待機しているかどうかに関わらず呼び出される可能性がありました。これは、同期プリミティブの一般的な問題である「スプリアスウェイクアップ(Spurious Wakeup)」や「ロストウェイクアップ(Lost Wakeup)」につながる可能性があります。
- スプリアスウェイクアップ: 待機しているスレッドが、シグナルが送られていないにも関わらず目覚めてしまう現象。これにより、不必要な処理が実行されたり、ロジックが複雑になったりします。
- ロストウェイクアップ: シグナルが送られたにも関わらず、待機しているスレッドがそのシグナルを受け取れずに永遠に待機し続けてしまう現象。これは、シグナルが送られた時点でまだ待機状態に入っていない、あるいは待機状態から一時的に離脱しているスレッドに対してシグナルが送られた場合に発生し得ます。
このコミットは、sched.stopped
の notewakeup
が stoptheworld
が実際に待機している場合にのみ行われるように、waitstop
というフラグを導入することで、これらの問題を解決し、STW処理の信頼性を向上させることを目的としています。
前提知識の解説
Goランタイムとスケジューラ
Goプログラムは、Goランタイム上で動作します。Goランタイムは、ゴルーチン(軽量スレッド)のスケジューリング、メモリ管理(ガベージコレクション)、チャネル通信など、プログラムの実行に必要な低レベルの機能を提供します。 Goスケジューラは、M(Machine、OSスレッド)、P(Processor、論理プロセッサ)、G(Goroutine)という3つのエンティティを使用して、ゴルーチンを効率的にOSスレッドにマッピングし、実行します。
Stop The World (STW)
STWは、ガベージコレクション(GC)などの特定のランタイム操作中に、すべてのゴルーチンの実行を一時的に停止させるメカニズムです。これにより、GCがメモリの状態を一貫してスキャンし、安全にクリーンアップできるようになります。STWは、アプリケーションの応答性に影響を与える可能性があるため、GoランタイムはSTWの時間を最小限に抑えるように設計されています。
Note
型
Goランタイムの内部では、Note
型は低レベルの同期プリミティブとして使用されます。これは、OSのセマフォや条件変数に似た機能を提供します。
Note
には主に以下の操作があります。
noteclear(Note *n)
:Note
の状態をクリアします。notesleep(Note *n)
:Note
がシグナルされるまで現在のスレッドをスリープさせます。notewakeup(Note *n)
:Note
をシグナルし、notesleep
で待機しているスレッドをウェイクアップします。
Note
は、特定のイベントが発生したことを別のゴルーチンやOSスレッドに通知するために使用されます。
技術的詳細
このコミットの核心は、sched
構造体に waitstop
という新しい int32
型のフィールドを追加し、このフラグを使って sched.stopped
Note
の notewakeup
呼び出しを条件付きにすることです。
-
struct Sched
へのwaitstop
フィールドの追加:sched
構造体はGoランタイムのグローバルスケジューラの状態を保持します。ここにint32 waitstop;
が追加されました。このフラグは、stoptheworld
関数がsched.stopped
Note
で待機しようとしていることを示すために使用されます。 -
stoptheworld
関数でのwaitstop
の設定:stoptheworld
関数は、すべてのゴルーチンを停止させる処理を開始します。この関数内で、スケジューラが他のゴルーチンが停止するのを待機するループに入るとき、noteclear(&sched.stopped);
の直後にsched.waitstop = 1;
が設定されます。 これは、「私は今からsched.stopped
で待機しますよ」という意図を明確に示します。このフラグを設定した後、スケジューラのロックを解放し、notesleep(&sched.stopped);
を呼び出して待機状態に入ります。 -
nextgandunlock
関数でのwaitstop
のチェックとnotewakeup
の呼び出し:nextgandunlock
関数は、現在のM(OSスレッド)が次に実行するゴルーチンを選択し、スケジューラのロックを解放する役割を担います。この関数は、ゴルーチンが実行を完了したり、ブロックしたりする際に呼び出される可能性があります。 変更前は、この関数内で無条件にnotewakeup(&sched.stopped);
が呼び出されていました。しかし、変更後はif(sched.waitstop)
という条件が追加されました。 この条件が真(つまり、stoptheworld
がsched.stopped
で待機している)の場合にのみ、sched.waitstop = 0;
と設定してフラグをクリアし、その後notewakeup(&sched.stopped);
を呼び出します。
このメカニズムにより、notewakeup(&sched.stopped)
は、stoptheworld
が実際にそのシグナルを期待している場合にのみ発生するようになります。これにより、ロストウェイクアップのリスクが軽減され、stoptheworld
処理の同期がより堅牢になります。
コアとなるコードの変更箇所
変更は src/runtime/proc.c
ファイルに集中しています。
--- a/src/runtime/proc.c
+++ b/src/runtime/proc.c
@@ -59,6 +59,7 @@ struct Sched {
int32 predawn; // running initialization, don't run new gs.
Note stopped; // one g can wait here for ms to stop
+ int32 waitstop; // after setting this flag
};
Sched sched;
@@ -352,7 +353,10 @@ nextgandunlock(void)
throw("all goroutines are asleep - deadlock!");
m->nextg = nil;
noteclear(&m->havenextg);
- notewakeup(&sched.stopped);
+ if(sched.waitstop) {
+ sched.waitstop = 0;
+ notewakeup(&sched.stopped);
+ }
unlock(&sched);
notesleep(&m->havenextg);
@@ -376,6 +380,7 @@ stoptheworld(void)
sched.mcpumax = 1;
while(sched.mcpu > 1) {
noteclear(&sched.stopped);
+ sched.waitstop = 1;
unlock(&sched);
notesleep(&sched.stopped);
lock(&sched);
コアとなるコードの解説
-
struct Sched
の変更:struct Sched { // ... 既存のフィールド ... Note stopped; // one g can wait here for ms to stop int32 waitstop; // after setting this flag };
sched
構造体にwaitstop
という新しいフィールドが追加されました。これは、stoptheworld
がsched.stopped
Note
で待機中であることを示すフラグとして機能します。 -
nextgandunlock
関数の変更:if(sched.waitstop) { sched.waitstop = 0; notewakeup(&sched.stopped); }
nextgandunlock
は、ゴルーチンが実行を終えたり、ブロックしたりする際に呼び出され、次のゴルーチンをスケジュールする準備をします。以前は無条件にnotewakeup(&sched.stopped)
を呼び出していましたが、この変更により、sched.waitstop
が1
の場合(つまり、stoptheworld
が待機している場合)にのみnotewakeup
が呼び出されるようになりました。notewakeup
を呼び出す前にsched.waitstop
を0
にリセットすることで、一度の待機に対して一度だけウェイクアップが行われることを保証します。 -
stoptheworld
関数の変更:while(sched.mcpu > 1) { noteclear(&sched.stopped); sched.waitstop = 1; // ここでフラグを設定 unlock(&sched); notesleep(&sched.stopped); // ここで待機 lock(&sched); }
stoptheworld
関数は、すべてのプロセッサ(P)が停止するのを待機するループを持っています。このループ内でnotesleep(&sched.stopped)
を呼び出す直前にsched.waitstop = 1;
が設定されます。これにより、stoptheworld
がsched.stopped
で待機する準備ができたことをwaitstop
フラグを通じて他の部分(nextgandunlock
など)に通知します。
この一連の変更により、sched.stopped
Note
のシグナルが、それを必要とする stoptheworld
処理に対してのみ正確に送られるようになり、ランタイムの同期メカニズムの堅牢性が向上しました。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Goランタイムのソースコード: https://github.com/golang/go/tree/master/src/runtime
- Goスケジューラに関する解説記事 (例: "Go's work-stealing scheduler"): https://rakyll.org/scheduler/
参考にした情報源リンク
- Goランタイムのソースコード (
src/runtime/proc.c
) - 同期プリミティブ(条件変数、セマフォ)に関する一般的な知識
- GoのガベージコレクションとStop The Worldに関する一般的な解説記事