[インデックス 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.stoppedNoteで待機しようとしていることを示すために使用されます。 -
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.stoppedNoteで待機中であることを示すフラグとして機能します。 -
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に関する一般的な解説記事