[インデックス 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つのファイルに影響を与えます。
src/pkg/runtime/runtime.h
:notetsleep
関数のプロトタイプ宣言がvoid
からbool
に変更され、コメントに// false - timeout
が追加されています。src/pkg/runtime/lock_futex.c
とsrc/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
関数の戻り値のセマンティクスを変更し、タイムアウトの有無を明示的に伝えるようにした点です。
-
関数シグネチャの変更:
runtime.h
において、runtime·notetsleep
の戻り値の型がvoid
からbool
に変更されました。これにより、関数がブール値を返すことが明確になります。コメント// false - timeout
は、false
がタイムアウトを意味することを簡潔に示しています。 -
lock_futex.c
とlock_sema.c
における実装の変更:- 即時成功ケース:
if(ns < 0)
(無限待機)の場合、またはif(runtime·atomicload((uint32*)&n->key) != 0)
(Note
が既に通知済み)の場合、以前はreturn;
で関数を終了していましたが、これらがreturn true;
に変更されました。これは、これらのケースではタイムアウトが発生せず、待機が「成功」したと見なされるためです。 lock_futex.c
の最終的な戻り値:lock_futex.c
のruntime·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のプリエンプティブスケジューリングに関する議論(このコミットの背景にある大きな変更の一部):
- Go's new non-cooperative preemption (Go 1.14で導入された非協調的プリエンプションに関する公式ブログ記事)
- Proposal: Non-cooperative preemption (Goのプリエンプションに関する設計ドキュメント)