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

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

このコミットは、Goランタイムのsrc/pkg/runtime/proc.cファイルに影響を与えています。このファイルはGoランタイムのスケジューラとプロセスの管理に関するコアロジックを含んでいます。具体的には、ゴルーチンのスケジューリング、P(プロセッサ)の管理、システムコールからの復帰処理などが実装されています。

コミット

commit 658d19a53f26865549653fb16f80a47ae552b4f0
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Jul 31 20:09:03 2013 +0400

    runtime: do not park sysmon thread if any goroutines are running
    Sysmon thread parks if no goroutines are running (runtime.sched.npidle ==
    runtime.gomaxprocs).
    Currently it's unparked when a goroutine enters syscall, it was enough
    to retake P's from blocking syscalls.
    But it's not enough for reliable goroutine preemption. We need to ensure that
    sysmon runs if any goroutines are running.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/12176043

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

https://github.com/golang/go/commit/658d19a53f26865549653fb16f80a47ae552b4f0

元コミット内容

runtime: do not park sysmon thread if any goroutines are running
Sysmon thread parks if no goroutines are running (runtime.sched.npidle ==
runtime.gomaxprocs).
Currently it's unparked when a goroutine enters syscall, it was enough
to retake P's from blocking syscalls.
But it's not enough for reliable goroutine preemption. We need to ensure that
sysmon runs if any goroutines are running.

変更の背景

このコミットの背景には、Goランタイムにおけるゴルーチン(goroutine)のプリエンプション(preemption、横取り)の信頼性向上が挙げられます。

Go 1.2(このコミットが作成された2013年頃のバージョン)以前のGoスケジューラは、基本的に協調的(cooperative)でした。これは、ゴルーチンが自発的に制御を譲る(例えばI/O操作中やruntime.Gosched()の明示的な呼び出し時)場合にのみ、スケジューラが別のゴルーチンに切り替えることを意味します。しかし、CPUバウンドな処理を行うゴルーチンが無限ループに陥った場合、他のゴルーチンが実行機会を得られず、システム全体が停止する可能性がありました。

sysmon(システムモニター)スレッドは、Goランタイム内で独立して動作するバックグラウンドスレッドであり、ガベージコレクションのトリガー、ネットワークポーラーの実行、そして重要なことに、長時間実行されているゴルーチンのプリエンプションチェックを担当しています。

以前のsysmonスレッドは、実行中のゴルーチンがない場合(runtime.sched.npidle == runtime.gomaxprocs、つまり全てのPがアイドル状態である場合)に自身をパーク(park、一時停止)していました。そして、ゴルーチンがシステムコールに入ったときにアンパーク(unpark、再開)される仕組みでした。これは、ブロッキングシステムコールからPを再取得するためには十分でしたが、ゴルーチンのプリエンプションを確実に実行するためには不十分でした。

問題は、sysmonスレッドがパークされている間に、CPUバウンドなゴルーチンが長時間実行され続けると、sysmonがプリエンプションチェックを実行できず、他のゴルーチンが飢餓状態に陥る可能性があったことです。このコミットは、実行中のゴルーチンが一つでも存在する限り、sysmonスレッドがパークされないようにすることで、この問題を解決し、より信頼性の高いゴルーチンプリエンプションを保証することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念とメカニズムを理解しておく必要があります。

  • Goランタイム (Go Runtime): Goプログラムの実行を管理するシステム。スケジューラ、ガベージコレクタ、メモリ管理などが含まれます。C言語で実装された部分が多く、OSとGoプログラムの橋渡しをします。
  • ゴルーチン (Goroutine): Goにおける軽量な実行単位。OSのスレッドよりもはるかに軽量で、数百万個作成することも可能です。Goランタイムのスケジューラによって管理されます。
  • P (Processor): 論理プロセッサ。Goスケジューラがゴルーチンを実行するために必要なコンテキスト(ローカルキュー、実行中のゴルーチンなど)を保持します。GOMAXPROCS環境変数によって設定されるPの数は、同時に実行できるゴルーチンの最大数を決定します。
  • M (Machine): OSのスレッド。Pとゴルーチンを実行するための実際のOSスレッドです。MはPをアタッチし、そのP上でゴルーチンを実行します。
  • G (Goroutine): ゴルーチンそのもの。実行スタックや状態を保持します。
  • sysmonスレッド (System Monitor Thread): Goランタイムが起動時に作成する特別なOSスレッド。Pにアタッチされず、独立して動作します。主な役割は以下の通りです。
    • ガベージコレクションのトリガー: GCの実行タイミングを監視し、必要に応じてGCをトリガーします。
    • ネットワークポーラーの実行: ネットワークI/Oの準備ができたことを監視し、対応するゴルーチンを再開します。
    • 長時間実行ゴルーチンのプリエンプションチェック: 長時間実行されているゴルーチンを検出し、プリエンプションを促します(Go 1.2時点では協調的プリエンプション)。
    • デッドロック検出: デッドロック状態を検出します。
  • プリエンプション (Preemption): スケジューラが、実行中のタスク(この場合はゴルーチン)の実行を中断し、別のタスクにCPUを割り当てること。Go 1.2時点では、sysmonがプリエンプションフラグを立て、ゴルーチンが関数呼び出しの開始時(セーフポイント)に自発的に制御を譲る「協調的プリエンプション」でした。Go 1.14で非同期プリエンプションが導入され、より強制的なプリエンプションが可能になりました。
  • システムコール (System Call): プログラムがOSの機能(ファイルI/O、ネットワーク通信など)を利用するためにOSに要求を出すこと。Goのゴルーチンがシステムコールに入ると、そのゴルーチンはブロッキング状態になり、MはPを解放して他のゴルーチンを実行できるようになります。
  • runtime.sched: Goランタイムのスケジューラに関するグローバルな状態を保持する構造体。
    • runtime.sched.npidle: 現在アイドル状態のPの数。
    • runtime.gomaxprocs: 設定されているPの総数。
    • runtime.sched.sysmonwait: sysmonスレッドが待機中(パーク中)であるかを示すフラグ。
    • runtime.sched.sysmonnote: sysmonスレッドをウェイクアップするための通知オブジェクト(runtime.noteclear, runtime.notewakeup, runtime.notesleepで使用)。
  • runtime.atomicload, runtime.atomicstore: アトミック操作。複数のゴルーチンから同時にアクセスされてもデータ競合が発生しないように、変数の読み書きを不可分に行うための関数。
  • runtime.notewakeup: 指定された通知オブジェクトで待機しているスレッドをウェイクアップする関数。

技術的詳細

このコミットの核心は、sysmonスレッドのアンパーク(再開)ロジックの変更にあります。

従来のsysmonスレッドは、全てのPがアイドル状態(runtime.sched.npidle == runtime.gomaxprocs)になった場合にパークしていました。これは、実行すべきゴルーチンがないため、sysmonも特にやることがないという判断に基づいています。そして、ゴルーチンがシステムコールに入り、MがPを解放する際に、そのMが新しいPを取得できない場合にsysmonをアンパークしていました。このアンパークは、ブロッキングシステムコールからPを再取得する(例えば、システムコールから戻ってきたゴルーチンが実行を再開するためにPが必要な場合)ためには十分でした。

しかし、この挙動には問題がありました。もし全てのPがゴルーチンを実行中で、かつそれらのゴルーチンがシステムコールに入らずにCPUバウンドな処理を長時間行っている場合、sysmonスレッドはパークされたままになり、プリエンプションチェックを実行できません。結果として、長時間実行ゴルーチンが他のゴルーチンを飢餓状態に陥らせる可能性がありました。

このコミットは、この問題を解決するために、ゴルーチンがシステムコールから復帰する際に、sysmonスレッドが待機中(runtime.atomicload(&runtime.sched.sysmonwait)が真)であれば、sysmonスレッドを強制的にアンパークするロジックを追加しています。

具体的には、exitsyscallfastexitsyscall0という2つの関数に修正が加えられています。これらの関数は、ゴルーチンがシステムコールから復帰する際に呼び出されます。

  • exitsyscallfast: システムコールから高速に復帰できる場合のパス。
  • exitsyscall0: システムコールから復帰する際の一般的なパス。

これらの関数内で、ゴルーチンがシステムコールから戻り、Pを取得しようとする際に、sysmonが待機中であれば、runtime.atomicstore(&runtime.sched.sysmonwait, 0)で待機フラグをクリアし、runtime.notewakeup(&runtime.sched.sysmonnote)を呼び出してsysmonスレッドをウェイクアップします。

これにより、たとえ全てのPがゴルーチンを実行中であっても、いずれかのゴルーチンがシステムコールから復帰するたびにsysmonスレッドがウェイクアップされる機会が生まれます。これにより、sysmonは定期的にプリエンプションチェックを実行できるようになり、長時間実行ゴルーチンによる飢餓状態をより効果的に防ぐことが可能になります。

この変更は、Goランタイムのスケジューリングの公平性を向上させ、特にCPUバウンドなワークロードにおいて、より安定したゴルーチンの実行を保証するために重要でした。

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

変更はsrc/pkg/runtime/proc.cファイルに集中しており、以下の2つの箇所にコードが追加されています。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1536,6 +1536,10 @@ exitsyscallfast(void)
 	if(runtime·sched.pidle) {
 		runtime·lock(&runtime·sched);
 		p = pidleget();
+		if(p && runtime·atomicload(&runtime·sched.sysmonwait)) {
+			runtime·atomicstore(&runtime·sched.sysmonwait, 0);
+			runtime·notewakeup(&runtime·sched.sysmonnote);
+		}
 		runtime·unlock(&runtime·sched);
 		if(p) {
 			acquirep(p);
@@ -1559,6 +1563,10 @@ exitsyscall0(G *gp)
 	p = pidleget();
 	if(p == nil)
 		globrunqput(gp);
+	else if(runtime·atomicload(&runtime·sched.sysmonwait)) {
+		runtime·atomicstore(&runtime·sched.sched.sysmonwait, 0);
+		runtime·notewakeup(&runtime·sched.sysmonnote);
+	}
 	runtime·unlock(&runtime·sched);
 	if(p) {
 		acquirep(p);

コアとなるコードの解説

exitsyscallfast 関数内の変更

		if(p && runtime·atomicload(&runtime·sched.sysmonwait)) {
			runtime·atomicstore(&runtime·sched.sysmonwait, 0);
			runtime·notewakeup(&runtime·sched.sysmonnote);
		}
  • このコードブロックは、ゴルーチンがシステムコールから高速パスで復帰する際に実行されます。
  • if(p && runtime·atomicload(&runtime·sched.sysmonwait))
    • p: システムコールから復帰したゴルーチンがP(プロセッサ)を取得できたかどうかを示します。pnilでない場合、Pが利用可能であることを意味します。
    • runtime·atomicload(&runtime·sched.sysmonwait): sysmonスレッドが現在待機中(パーク中)であるかを示すフラグをアトミックに読み込みます。
    • この条件は、「Pが利用可能であり、かつsysmonスレッドが待機中である」場合に真となります。
  • runtime·atomicstore(&runtime·sched.sysmonwait, 0);
    • sysmonwaitフラグを0に設定し、sysmonスレッドが待機状態ではないことをアトミックにマークします。
  • runtime·notewakeup(&runtime·sched.sysmonnote);
    • sysmonnote通知オブジェクトを使用して、待機中のsysmonスレッドをウェイクアップします。これにより、sysmonスレッドは実行を再開し、プリエンプションチェックなどのタスクを実行できるようになります。

exitsyscall0 関数内の変更

	else if(runtime·atomicload(&runtime·sched.sysmonwait)) {
		runtime·atomicstore(&runtime·sched.sysmonwait, 0);
		runtime·notewakeup(&runtime·sched.sysmonnote);
	}
  • このコードブロックは、ゴルーチンがシステムコールから一般的なパスで復帰する際に実行されます。
  • p = pidleget(); の後に実行されます。pidleget()はアイドル状態のPを取得しようとします。
  • if(p == nil) globrunqput(gp);else if節として追加されています。これは、Pが取得できなかった場合(p == nil)はゴルーチンをグローバル実行キューに戻し、そうでない場合(Pが取得できた場合)にこのelse ifの条件を評価することを意味します。
  • runtime·atomicload(&runtime·sched.sysmonwait): sysmonスレッドが現在待機中(パーク中)であるかを示すフラグをアトミックに読み込みます。
  • この条件は、「Pが取得できたが、sysmonスレッドが待機中である」場合に真となります。
  • runtime·atomicstore(&runtime·sched.sysmonwait, 0);
    • sysmonwaitフラグを0に設定し、sysmonスレッドが待機状態ではないことをアトミックにマークします。
  • runtime·notewakeup(&runtime·sched.sysmonnote);
    • sysmonnote通知オブジェクトを使用して、待機中のsysmonスレッドをウェイクアップします。

これらの変更により、ゴルーチンがシステムコールから復帰するたびに、sysmonスレッドがパーク状態であればウェイクアップされる機会が提供されます。これにより、実行中のゴルーチンが存在する限りsysmonが活動し続けることが保証され、Goランタイムのプリエンプションメカニズムの信頼性が向上しました。

関連リンク

参考にした情報源リンク