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

[インデックス 16429] ファイルの概要

このコミットは、Goランタイムにおけるnotetsleep関数の振る舞いを変更し、タイムアウトが発生した場合にfalseを返すように修正するものです。これは、Goのプリエンプティブスケジューラがstoptheworldフェーズ中に、タイムアウトを検知してM(マシン、OSスレッド)を再プリエンプトできるようにするために必要とされます。

コミット

commit e932c2035f01f76f614750af022d2f3975146191
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed May 29 11:49:45 2013 +0400

    runtime: make notetsleep() return false if timeout happens
    This is needed for preemptive scheduler, because during
    stoptheworld we want to wait with timeout and re-preempt
    M's on timeout.

    R=golang-dev, remyoudompheng, iant
    CC=golang-dev
    https://golang.org/cl/9375043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/e932c2035f01f76f614750af022d2f3975146191

元コミット内容

Goランタイムのnotetsleep()関数は、指定された時間(ns)だけ待機するか、Noteが通知されるまで待機する低レベルの同期プリミティブです。このコミット以前は、notetsleep()voidを返しており、待機がタイムアウトによって終了したのか、それともNoteが通知されたことによって終了したのかを呼び出し元が直接区別する方法がありませんでした。

変更の背景

この変更の主な背景には、Goランタイムにおけるプリエンプティブスケジューラの導入があります。Goのスケジューラは、Goルーチン(G)をOSスレッド(M)上で実行し、Mはプロセッサ(P)に割り当てられます。初期のGoスケジューラは協調的(cooperative)であり、Goルーチンが明示的にスケジューラに制御を返す(例えば、I/O操作やチャネル操作でブロックする)まで、同じM上で実行され続ける傾向がありました。これにより、計算量の多いGoルーチンが長時間CPUを占有し、他のGoルーチンの実行を妨げる「スケジューリングの飢餓」が発生する可能性がありました。

この問題を解決するために、Goはプリエンプティブスケジューラを導入しました。プリエンプティブスケジューラは、Goルーチンが長時間実行されている場合に、強制的にその実行を中断し、他のGoルーチンにCPUを割り当てるメカニズムです。このプリエンプションを実現するためには、ランタイムが特定のGoルーチン(またはM)の実行を一時的に停止させる必要があります。この停止処理は「stoptheworld」と呼ばれます。

stoptheworldは、ガベージコレクション(GC)などの重要なランタイム操作を実行する際にも使用されます。stoptheworld中、すべてのGoルーチンは安全なポイントで停止され、ランタイムはGCなどの作業を中断なく実行できます。

このコミットの具体的な背景は、プリエンプティブスケジューラがstoptheworld中にMをプリエンプトする際に、notetsleep()のタイムアウト機能を利用する必要があったためです。Mが特定のイベントを待機している場合、スケジューラはタイムアウト付きで待機し、タイムアウトが発生した場合にはそのMを再プリエンプトして、他のGoルーチンにCPUを割り当て直す必要がありました。notetsleep()がタイムアウトの有無を返さない場合、このロジックを正確に実装することが困難でした。

前提知識の解説

  • Goランタイム: Goプログラムの実行を管理するシステム。スケジューラ、ガベージコレクタ、メモリ管理などが含まれます。
  • Goスケジューラ (M, P, G):
    • G (Goroutine): Goにおける軽量な実行単位。ユーザーが書く並行処理の最小単位。
    • M (Machine): OSスレッド。Goルーチンを実行する実際のOSスレッド。
    • P (Processor): 論理プロセッサ。MがGを実行するために必要なコンテキスト。Pの数は通常、CPUコア数に等しい。
    • Goスケジューラは、GをPに割り当て、PがM上でGを実行する、というモデルで動作します。
  • プリエンプティブスケジューラ: 実行中のタスク(Goルーチン)を、そのタスクが自発的に制御を返さなくても、一定時間経過後や特定のイベント発生時に強制的に中断し、他のタスクにCPUを割り当てるスケジューリング方式。これにより、タスクの飢餓を防ぎ、システムの応答性を向上させます。
  • stoptheworld (STW): Goランタイムが、すべてのGoルーチンの実行を一時的に停止させるフェーズ。主にガベージコレクションのマーキングフェーズや、スケジューラの重要な変更(例えば、Mの再割り当て)など、ランタイムの状態が整合性を保つ必要がある場合に発生します。STW中は、Goプログラムの実行が完全に停止するため、その時間は最小限に抑える必要があります。
  • Note: Goランタイム内部で使用される低レベルの同期プリミティブ。OSのセマフォやイベントオブジェクトに似ており、Goルーチンが特定のイベントを待機したり、他のGoルーチンに通知したりするために使用されます。notesleepは待機、notewakeupは通知を行います。
  • futex (Fast Userspace muTEX): Linuxカーネルが提供する同期プリミティブ。ユーザー空間でロックやセマフォなどの同期機構を効率的に実装するために使用されます。競合がない場合はカーネルへのシステムコールを回避し、ユーザー空間で処理を完結させることができます。
  • sema (Semaphore): セマフォ。複数のプロセスやスレッドが共有リソースにアクセスする際の同期を制御するための変数。Goランタイムでは、futexが利用できない環境や、より一般的なセマフォベースの同期が必要な場合に使用されます。

技術的詳細

このコミットは、Goランタイムの低レベル同期プリミティブであるnotetsleep関数のシグネチャと実装を変更します。

変更前: void runtime·notetsleep(Note *n, int64 ns) この関数は、Note *nで指定されたイベントをnsナノ秒間待機します。nsが負の場合、無限に待機します。戻り値はvoidであり、待機がタイムアウトしたのか、それともNoteが通知されたのかを区別できませんでした。

変更後: bool runtime·notetsleep(Note *n, int64 ns) この関数は、boolを返すようになりました。

  • trueを返す場合: Noteが通知されたか、待機開始時点で既に通知済みであったことを意味します。つまり、タイムアウトせずに待機が終了したことを示します。
  • falseを返す場合: 指定されたns期間が経過し、タイムアウトによって待機が終了したことを意味します。

この変更は、主に以下の2つのファイルに影響を与えます。

  1. src/pkg/runtime/runtime.h: notetsleep関数のプロトタイプ宣言がvoidからboolに変更され、コメントに// false - timeoutが追加されています。
  2. src/pkg/runtime/lock_futex.csrc/pkg/runtime/lock_sema.c: これらはそれぞれ、Linuxのfutexと一般的なセマフォ(またはそれらのエミュレーション)を使用してNoteを実装しているファイルです。これらのファイル内のruntime·notetsleep関数の実装が、新しい戻り値のセマンティクスに合わせて修正されています。

具体的な変更点としては、以下のロジックが導入されています。

  • ns < 0(無限待機)の場合、または待機開始時点でNoteが既に通知済みの場合(n->keyが非ゼロの場合)、即座にtrueを返します。これは、タイムアウトが発生しないケースです。
  • タイムアウトが発生した場合(待機期間が終了してもNoteが通知されなかった場合)、falseを返します。
  • Noteが通知された場合(待機中にn->keyが設定された場合)、trueを返します。

この明確な戻り値により、呼び出し元(特にプリエンプティブスケジューラ)は、待機がタイムアウトによって終了したのか、それともイベントによって終了したのかを正確に判断できるようになります。これにより、タイムアウト時にMを再プリエンプトするという、より洗練されたスケジューリングロジックを実装することが可能になります。

コアとなるコードの変更箇所

src/pkg/runtime/runtime.h

--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -862,7 +862,7 @@ void	runtime·unlock(Lock*);
 void	runtime·noteclear(Note*);
 void	runtime·notesleep(Note*);
 void	runtime·notewakeup(Note*);
-void	runtime·notetsleep(Note*, int64);
+bool	runtime·notetsleep(Note*, int64);  // false - timeout

 /*
  * low-level synchronization for implementing the above

src/pkg/runtime/lock_futex.c

--- a/src/pkg/runtime/lock_futex.c
+++ b/src/pkg/runtime/lock_futex.c
@@ -127,18 +127,18 @@ runtime·notesleep(Note *n)
 		runtime·setprof(true);
 }

-void
+bool
 runtime·notetsleep(Note *n, int64 ns)
 {
 	int64 deadline, now;

 	if(ns < 0) {
 		runtime·notesleep(n);
-		return;
+		return true;
 	}

 	if(runtime·atomicload((uint32*)&n->key) != 0)
-		return;
+		return true;

 	if(m->profilehz > 0)
 		runtime·setprof(false);
@@ -154,4 +154,5 @@ runtime·notetsleep(Note *n, int64 ns)
 	}
 	if(m->profilehz > 0)
 		runtime·setprof(true);
+	return runtime·atomicload((uint32*)&n->key) != 0;
 }

src/pkg/runtime/lock_sema.c

--- a/src/pkg/runtime/lock_sema.c
+++ b/src/pkg/runtime/lock_sema.c
@@ -161,7 +161,7 @@ runtime·notesleep(Note *n)
 		runtime·setprof(true);
 }

-void
+bool
 runtime·notetsleep(Note *n, int64 ns)
 {
 	M *mp;
@@ -169,7 +169,7 @@ runtime·notetsleep(Note *n, int64 ns)

 	if(ns < 0) {
 		runtime·notesleep(n);
-		return;
+		return true;
 	}

 	if(m->waitsema == 0)
@@ -179,7 +179,7 @@ runtime·notetsleep(Note *n, int64 ns)
 	if(!runtime·casp((void**)&n->key, nil, m)) {  // must be LOCKED (got wakeup already)
 		if(n->key != LOCKED)
 			runtime·throw("notetsleep - waitm out of sync");
-		return;
+		return true;
 	}

 	if(m->profilehz > 0)
@@ -192,7 +192,7 @@ runtime·notetsleep(Note *n, int64 ns)
 			// Done.
 			if(m->profilehz > 0)
 				runtime·setprof(true);
-			return;
+			return true;
 		}

 		// Interrupted or timed out.  Still registered.  Semaphore not acquired.
@@ -216,13 +216,13 @@ runtime·notetsleep(Note *n, int64 ns)
 		if(mp == m) {
 			// No wakeup yet; unregister if possible.
 			if(runtime·casp((void**)&n->key, mp, nil))
-				return;
+				return false;
 		} else if(mp == (M*)LOCKED) {
 			// Wakeup happened so semaphore is available.
 			// Grab it to avoid getting out of sync.
 			if(runtime·semasleep(-1) < 0)
 				runtime·throw("runtime: unable to acquire - semaphore out of sync");
-			return;
+			return true;
 		} else {
 			runtime·throw("runtime: unexpected waitm - semaphore out of sync");
 		}

コアとなるコードの解説

このコミットの核心は、runtime·notetsleep関数の戻り値のセマンティクスを変更し、タイムアウトの有無を明示的に伝えるようにした点です。

  1. 関数シグネチャの変更: runtime.hにおいて、runtime·notetsleepの戻り値の型がvoidからboolに変更されました。これにより、関数がブール値を返すことが明確になります。コメント// false - timeoutは、falseがタイムアウトを意味することを簡潔に示しています。

  2. lock_futex.clock_sema.cにおける実装の変更:

    • 即時成功ケース: if(ns < 0)(無限待機)の場合、またはif(runtime·atomicload((uint32*)&n->key) != 0)Noteが既に通知済み)の場合、以前はreturn;で関数を終了していましたが、これらがreturn true;に変更されました。これは、これらのケースではタイムアウトが発生せず、待機が「成功」したと見なされるためです。
    • lock_futex.cの最終的な戻り値: lock_futex.cruntime·notetsleepの最後では、return runtime·atomicload((uint32*)&n->key) != 0;が追加されました。これは、待機が終了した時点でn->keyが非ゼロであれば(つまり、Noteが通知されていれば)trueを返し、そうでなければ(タイムアウトしていれば)falseを返すというロジックを直接的に表現しています。
    • lock_sema.cのタイムアウトケース: lock_sema.cでは、セマフォの待機がタイムアウトした場合のパスで、以前のreturn;return false;に変更されました。これは、タイムアウトを明示的に示すための変更です。
    • lock_sema.cの通知済みケース: lock_sema.cで、Noteが通知された(mp == (M*)LOCKED)場合のパスで、以前のreturn;return true;に変更されました。これは、タイムアウトせずに待機が終了したことを明示的に示すための変更です。

これらの変更により、notetsleepの呼び出し元は、戻り値のboolをチェックするだけで、待機がタイムアウトしたのか、それともイベントによって解除されたのかを確実に判断できるようになりました。これは、Goのプリエンプティブスケジューラがstoptheworld中にMを効率的に管理し、タイムアウト時に適切なアクション(Mの再プリエンプト)を実行するために不可欠な機能です。

関連リンク

  • Goのプリエンプティブスケジューリングに関する議論(このコミットの背景にある大きな変更の一部):

参考にした情報源リンク