[インデックス 19093] ファイルの概要
このコミットは、GoランタイムにおけるPlan 9オペレーティングシステム向けのsemasleep
関数のバグ修正に関するものです。具体的には、semasleep
が非常に短い時間(例えば100マイクロ秒)のスリープを要求された際に、内部的に計算されるミリ秒単位の時間がゼロになり、結果としてCPUを浪費するビジーループ(busy-wait loop)に陥る問題を解決しています。
コミット
commit 5a513061709dc7513a54635bd6bc04c483ceffea
Author: David du Colombier <0intro@gmail.com>
Date: Thu Apr 10 06:36:20 2014 +0200
runtime: fix semasleep on Plan 9
If you pass ns = 100,000 to this function, timediv will
return ms = 0. tsemacquire in /sys/src/9/port/sysproc.c
will return immediately when ms == 0 and the semaphore
cannot be acquired immediately - it doesn't sleep - so
notetsleep will spin, chewing cpu and repeatedly reading
the time, until the 100us have passed.
Thanks to the time reads it won't take too many iterations,
but whatever we are waiting for does not get a chance to
run. Eventually the notetsleep spin loop returns and we
end up in the stoptheworld spin loop - actually a sleep
loop but we're not doing a good job of sleeping.
After 100ms or so of this, the kernel says enough and
schedules a different thread. That thread manages to do
whatever we're waiting for, and the spinning in the other
thread stops. If tsemacquire had actually slept, this
would have happened much quicker.
Many thanks to Russ Cox for help debugging.
LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/86210043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5a513061709dc7513a54635bd6bc04c483ceffea
元コミット内容
このコミットは、Goランタイムのos_plan9.c
ファイルにおけるruntime·semasleep
関数の修正です。具体的には、ns
(ナノ秒)をミリ秒に変換した結果ms
が0
になった場合、ms
の値を1
に設定するという変更が加えられています。
--- a/src/pkg/runtime/os_plan9.c
+++ b/src/pkg/runtime/os_plan9.c
@@ -283,6 +283,8 @@ runtime·semasleep(int64 ns)
if(ns >= 0) {
ms = runtime·timediv(ns, 1000000, nil);
+ if(ms == 0)
+ ms = 1;
ret = runtime·plan9_tsemacquire(&m->waitsemacount, ms);
if(ret == 1)
return 0; // success
変更の背景
Goランタイムのsemasleep
関数は、指定されたナノ秒間、現在のゴルーチンをスリープさせるために使用されます。この関数は内部的にPlan 9オペレーティングシステムのセマフォ機構を利用しています。
問題は、semasleep
に非常に短い時間(例: 100,000ナノ秒、つまり100マイクロ秒)が渡された場合に発生しました。このナノ秒をミリ秒に変換するruntime·timediv
関数は、結果としてms = 0
を返していました。
Plan 9のtsemacquire
システムコール(/sys/src/9/port/sysproc.c
に実装されている)は、タイムアウト値ms
が0
の場合、セマフォが即座に取得できないと判断すると、スリープせずにすぐに制御を返します。この挙動により、notetsleep
(Goランタイムが内部で利用するスリープ機構)がセマフォの解放を待つ際に、実際にはスリープせず、CPUを消費しながら繰り返し時間を確認するビジーループ(busy-wait loop)に陥っていました。
このビジーループは、CPUを無駄に消費するだけでなく、他の実行可能なスレッドがCPU時間を獲得する機会を奪い、システムの全体的なパフォーマンスを低下させていました。コミットメッセージによると、約100ミリ秒この状態が続くと、カーネルが介入して別のスレッドをスケジュールする事態に発展していました。もしtsemacquire
が適切にスリープしていれば、このような問題は発生せず、より迅速に処理が進んだはずです。
この修正は、このような非効率なビジーループを防ぎ、GoランタイムがPlan 9上でより効率的に動作するようにするために導入されました。
前提知識の解説
- Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのシステムです。スケジューラ、ガベージコレクタ、メモリ管理、同期プリミティブなど、Go言語の並行処理モデルを支える重要な機能を提供します。
semasleep
のような関数は、Goランタイムが内部でゴルーチンのスケジューリングや同期のために使用するものです。 - Plan 9: ベル研究所で開発された分散オペレーティングシステムです。Go言語の設計者の一部(Ken Thompson, Rob Pike, Russ Coxなど)がPlan 9の開発にも深く関わっていたため、Go言語の設計思想やランタイムの一部にはPlan 9の影響が見られます。特に、ファイルシステムを介したリソースの抽象化や、シンプルなシステムコールインターフェースなどが特徴です。
- セマフォ (Semaphore): 複数のプロセスやスレッドが共有リソースにアクセスする際の同期を制御するためのプログラミング構成要素です。セマフォは、リソースの利用可能数を表すカウンタと、そのカウンタを操作する2つの原子操作(通常はP操作/wait/acquireとV操作/signal/release)から構成されます。
- P操作 (wait/acquire): セマフォのカウンタをデクリメントします。カウンタが負になる場合、操作を呼び出したプロセスはブロックされ、カウンタが正になるまで待機します。
- V操作 (signal/release): セマフォのカウンタをインクリメントします。待機しているプロセスがある場合、そのうちの1つを再開させます。
semasleep
(Goランタイム): Goランタイム内部で使用される関数で、指定された時間だけゴルーチンをスリープさせるためのセマフォベースのメカニズムです。これは、Goのsync.Mutex
などの高レベルな同期プリミティブの実装基盤となっています。tsemacquire
(Plan 9): Plan 9オペレーティングシステムにおけるセマフォ取得のためのシステムコールです。tsemacquire(addr, ms)
のように呼び出され、addr
で指定されたセマフォをms
ミリ秒間待機して取得しようとします。ms
が0
の場合、セマフォが即座に取得できない場合は待機せずにすぐに戻ります。- ビジーループ (Busy-waiting / Spinning): プロセスやスレッドが、ある条件が満たされるのを待つ際に、CPUを解放せずに繰り返し条件をチェックし続けることです。これはCPUサイクルを無駄に消費し、他のタスクの実行を妨げるため、通常は避けるべきパターンです。
- スリープ (Sleeping / Blocked Waiting): プロセスやスレッドが、ある条件が満たされるのを待つ際に、CPUをオペレーティングシステムに解放し、待機状態に入ることを指します。条件が満たされるか、指定された時間が経過すると、OSによって再開されます。CPUを効率的に利用できるため、ビジーループよりも推奨される待機方法です。
技術的詳細
このバグは、時間変換における丸め誤差と、Plan 9のtsemacquire
システムコールの特定の挙動の組み合わせによって引き起こされました。
-
時間変換の丸め誤差:
runtime·semasleep
関数は、引数としてナノ秒単位のns
を受け取ります。これをミリ秒単位のms
に変換するために、runtime·timediv(ns, 1000000, nil)
が使用されます。ここで、1000000
は1ミリ秒あたりのナノ秒数です。 例えば、ns = 100,000
(100マイクロ秒)の場合、100,000 / 1,000,000 = 0.1
となります。整数演算ではこの0.1
は0
に丸められます。したがって、ms
の値は0
になります。 -
tsemacquire
の挙動: Plan 9のtsemacquire
システムコールは、第2引数にタイムアウト値をミリ秒で取ります。このタイムアウト値が0
の場合、tsemacquire
はセマフォが即座に取得できないと判断すると、待機せずにすぐに0
を返します。これは、ノンブロッキングなセマフォ取得試行として機能します。 -
ビジーループの発生:
semasleep
は、tsemacquire
がセマフォを取得できなかった場合(つまりret != 1
の場合)、ループして再試行します。ms
が0
であるため、tsemacquire
は常に即座に制御を返し、スリープすることはありません。これにより、semasleep
は無限に(またはタイムアウトするまで)tsemacquire
を呼び出し続けるビジーループに陥ります。このループはCPUを消費し、他のゴルーチンやプロセスが実行される機会を奪います。 -
カーネルの介入: コミットメッセージが示唆するように、このビジーループが長時間続くと、Plan 9カーネルがシステムの公平性を保つために介入し、ビジーループ中のスレッドからCPUを奪い、他のスレッドにスケジュールを移すことがあります。これは、システムが正常に機能している証拠ではありますが、本来スリープすべき処理がビジーループに陥っているという根本的な問題を示しています。
修正のロジック:
修正は非常にシンプルかつ効果的です。ms
が0
と計算された場合、強制的にms
を1
に設定します。
ms = runtime·timediv(ns, 1000000, nil);
if(ms == 0)
ms = 1; // msが0の場合、最低1ミリ秒のスリープを保証
これにより、tsemacquire
は常に1
ミリ秒以上のタイムアウト値で呼び出されることになります(ただし、ns
が0
の場合を除く。ns=0
は即座にリターンすることを意味するため、この修正の影響を受けません)。ms
が1
であれば、tsemacquire
はセマフォが利用可能になるまで、または1ミリ秒が経過するまで適切にスリープします。これにより、CPUの無駄な消費が解消され、Goランタイムのスケジューリング効率が向上します。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/os_plan9.c
ファイルのruntime·semasleep
関数内の一箇所です。
// src/pkg/runtime/os_plan9.c
void
runtime·semasleep(int64 ns)
{
M *m;
int32 ms;
int32 ret;
m = runtime·m();
if(m->waitsemacount == 0) {
// m->waitsemacount is 0, so we are not waiting on a semaphore.
// This can happen if the semaphore was acquired before we called semasleep.
// In this case, we just return immediately.
return;
}
if(ns >= 0) {
ms = runtime·timediv(ns, 1000000, nil); // ナノ秒をミリ秒に変換
if(ms == 0) // msが0の場合のチェック
ms = 1; // 最低1ミリ秒のスリープを保証
ret = runtime·plan9_tsemacquire(&m->waitsemacount, ms);
if(ret == 1)
return 0; // success
}
// ... (後続のコードは変更なし)
}
コアとなるコードの解説
変更された行は以下の2行です。
if(ms == 0)
ms = 1;
-
ms = runtime·timediv(ns, 1000000, nil);
この行で、引数として渡されたナノ秒ns
をミリ秒ms
に変換しています。runtime·timediv
は、ns
を1000000
(1ミリ秒あたりのナノ秒数)で割ることでミリ秒を計算します。前述の通り、ns
が100,000
のような小さい値の場合、結果は0
になります。 -
if(ms == 0)
変換結果のms
が0
であるかどうかをチェックします。 -
ms = 1;
もしms
が0
であれば、ms
の値を1
に上書きします。これにより、runtime·plan9_tsemacquire
が呼び出される際に、タイムアウト値として最低でも1
ミリ秒が渡されることが保証されます。結果として、tsemacquire
は即座にリターンするのではなく、少なくとも1
ミリ秒間はセマフォの解放を待つ(つまりスリープする)挙動に変わります。これにより、CPUを消費するビジーループが回避され、Goランタイムのスケジューリングがより効率的になります。
この修正は、GoランタイムがPlan 9上で、短いスリープ要求に対しても適切にOSのセマフォ機構を利用し、CPUリソースを効率的に使用することを保証します。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Plan 9 from Bell Labs: https://9p.io/plan9/
- Goランタイムのセマフォ実装に関する議論(Russ Coxによるものなど): Goの内部セマフォは、Plan 9のセマフォ設計に影響を受けています。
参考にした情報源リンク
- Go runtime semasleep Plan 9: Web検索結果より、Goランタイムの
semasleep
がPlan 9のセマフォ実装に影響を受けていること、およびその目的(sleep/wakeupプリミティブ)について。 - Plan 9 tsemacquire: Web検索結果より、
tsemacquire
の挙動、特にタイムアウト値が0
の場合の即時リターンについて。 - Busy-waiting vs sleeping in operating systems: Web検索結果より、ビジーループとスリープの概念、それぞれの利点と欠点、CPU使用率への影響について。
- Go runtime semaphore implementation: Web検索結果より、Goランタイムが内部的にセマフォを使用していること、およびその目的について。
- Goのソースコード:
src/pkg/runtime/os_plan9.c
- Plan 9のソースコード:
/sys/src/9/port/sysproc.c
(tsemacquireの実装)