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

[インデックス 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.stoppednotewakeup (シグナル) が、stoptheworld が実際に待機しているかどうかに関わらず呼び出される可能性がありました。これは、同期プリミティブの一般的な問題である「スプリアスウェイクアップ(Spurious Wakeup)」や「ロストウェイクアップ(Lost Wakeup)」につながる可能性があります。

  • スプリアスウェイクアップ: 待機しているスレッドが、シグナルが送られていないにも関わらず目覚めてしまう現象。これにより、不必要な処理が実行されたり、ロジックが複雑になったりします。
  • ロストウェイクアップ: シグナルが送られたにも関わらず、待機しているスレッドがそのシグナルを受け取れずに永遠に待機し続けてしまう現象。これは、シグナルが送られた時点でまだ待機状態に入っていない、あるいは待機状態から一時的に離脱しているスレッドに対してシグナルが送られた場合に発生し得ます。

このコミットは、sched.stoppednotewakeupstoptheworld が実際に待機している場合にのみ行われるように、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 Notenotewakeup 呼び出しを条件付きにすることです。

  1. struct Sched への waitstop フィールドの追加: sched 構造体はGoランタイムのグローバルスケジューラの状態を保持します。ここに int32 waitstop; が追加されました。このフラグは、stoptheworld 関数が sched.stopped Note で待機しようとしていることを示すために使用されます。

  2. stoptheworld 関数での waitstop の設定: stoptheworld 関数は、すべてのゴルーチンを停止させる処理を開始します。この関数内で、スケジューラが他のゴルーチンが停止するのを待機するループに入るとき、noteclear(&sched.stopped); の直後に sched.waitstop = 1; が設定されます。 これは、「私は今から sched.stopped で待機しますよ」という意図を明確に示します。このフラグを設定した後、スケジューラのロックを解放し、notesleep(&sched.stopped); を呼び出して待機状態に入ります。

  3. nextgandunlock 関数での waitstop のチェックと notewakeup の呼び出し: nextgandunlock 関数は、現在のM(OSスレッド)が次に実行するゴルーチンを選択し、スケジューラのロックを解放する役割を担います。この関数は、ゴルーチンが実行を完了したり、ブロックしたりする際に呼び出される可能性があります。 変更前は、この関数内で無条件に notewakeup(&sched.stopped); が呼び出されていました。しかし、変更後は if(sched.waitstop) という条件が追加されました。 この条件が真(つまり、stoptheworldsched.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);

コアとなるコードの解説

  1. struct Sched の変更:

    struct Sched {
        // ... 既存のフィールド ...
        Note	stopped;	// one g can wait here for ms to stop
        int32 waitstop;	// after setting this flag
    };
    

    sched 構造体に waitstop という新しいフィールドが追加されました。これは、stoptheworldsched.stopped Note で待機中であることを示すフラグとして機能します。

  2. nextgandunlock 関数の変更:

    if(sched.waitstop) {
        sched.waitstop = 0;
        notewakeup(&sched.stopped);
    }
    

    nextgandunlock は、ゴルーチンが実行を終えたり、ブロックしたりする際に呼び出され、次のゴルーチンをスケジュールする準備をします。以前は無条件に notewakeup(&sched.stopped) を呼び出していましたが、この変更により、sched.waitstop1 の場合(つまり、stoptheworld が待機している場合)にのみ notewakeup が呼び出されるようになりました。notewakeup を呼び出す前に sched.waitstop0 にリセットすることで、一度の待機に対して一度だけウェイクアップが行われることを保証します。

  3. 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; が設定されます。これにより、stoptheworldsched.stopped で待機する準備ができたことを waitstop フラグを通じて他の部分(nextgandunlock など)に通知します。

この一連の変更により、sched.stopped Note のシグナルが、それを必要とする stoptheworld 処理に対してのみ正確に送られるようになり、ランタイムの同期メカニズムの堅牢性が向上しました。

関連リンク

参考にした情報源リンク

  • Goランタイムのソースコード (src/runtime/proc.c)
  • 同期プリミティブ(条件変数、セマフォ)に関する一般的な知識
  • GoのガベージコレクションとStop The Worldに関する一般的な解説記事