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

[インデックス 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(ナノ秒)をミリ秒に変換した結果ms0になった場合、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に実装されている)は、タイムアウト値ms0の場合、セマフォが即座に取得できないと判断すると、スリープせずにすぐに制御を返します。この挙動により、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ミリ秒間待機して取得しようとします。ms0の場合、セマフォが即座に取得できない場合は待機せずにすぐに戻ります。
  • ビジーループ (Busy-waiting / Spinning): プロセスやスレッドが、ある条件が満たされるのを待つ際に、CPUを解放せずに繰り返し条件をチェックし続けることです。これはCPUサイクルを無駄に消費し、他のタスクの実行を妨げるため、通常は避けるべきパターンです。
  • スリープ (Sleeping / Blocked Waiting): プロセスやスレッドが、ある条件が満たされるのを待つ際に、CPUをオペレーティングシステムに解放し、待機状態に入ることを指します。条件が満たされるか、指定された時間が経過すると、OSによって再開されます。CPUを効率的に利用できるため、ビジーループよりも推奨される待機方法です。

技術的詳細

このバグは、時間変換における丸め誤差と、Plan 9のtsemacquireシステムコールの特定の挙動の組み合わせによって引き起こされました。

  1. 時間変換の丸め誤差: runtime·semasleep関数は、引数としてナノ秒単位のnsを受け取ります。これをミリ秒単位のmsに変換するために、runtime·timediv(ns, 1000000, nil)が使用されます。ここで、1000000は1ミリ秒あたりのナノ秒数です。 例えば、ns = 100,000(100マイクロ秒)の場合、100,000 / 1,000,000 = 0.1となります。整数演算ではこの0.10に丸められます。したがって、msの値は0になります。

  2. tsemacquireの挙動: Plan 9のtsemacquireシステムコールは、第2引数にタイムアウト値をミリ秒で取ります。このタイムアウト値が0の場合、tsemacquireはセマフォが即座に取得できないと判断すると、待機せずにすぐに0を返します。これは、ノンブロッキングなセマフォ取得試行として機能します。

  3. ビジーループの発生: semasleepは、tsemacquireがセマフォを取得できなかった場合(つまりret != 1の場合)、ループして再試行します。ms0であるため、tsemacquireは常に即座に制御を返し、スリープすることはありません。これにより、semasleepは無限に(またはタイムアウトするまで)tsemacquireを呼び出し続けるビジーループに陥ります。このループはCPUを消費し、他のゴルーチンやプロセスが実行される機会を奪います。

  4. カーネルの介入: コミットメッセージが示唆するように、このビジーループが長時間続くと、Plan 9カーネルがシステムの公平性を保つために介入し、ビジーループ中のスレッドからCPUを奪い、他のスレッドにスケジュールを移すことがあります。これは、システムが正常に機能している証拠ではありますが、本来スリープすべき処理がビジーループに陥っているという根本的な問題を示しています。

修正のロジック: 修正は非常にシンプルかつ効果的です。ms0と計算された場合、強制的にms1に設定します。

		ms = runtime·timediv(ns, 1000000, nil);
		if(ms == 0)
			ms = 1; // msが0の場合、最低1ミリ秒のスリープを保証

これにより、tsemacquireは常に1ミリ秒以上のタイムアウト値で呼び出されることになります(ただし、ns0の場合を除く。ns=0は即座にリターンすることを意味するため、この修正の影響を受けません)。ms1であれば、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は、ns1000000(1ミリ秒あたりのナノ秒数)で割ることでミリ秒を計算します。前述の通り、ns100,000のような小さい値の場合、結果は0になります。

  • if(ms == 0) 変換結果のms0であるかどうかをチェックします。

  • ms = 1; もしms0であれば、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の実装)