[インデックス 16948] ファイルの概要
このコミットは、Goランタイムのシステムモニター(sysmon)スレッドの動作に関する重要な改善を導入しています。具体的には、sysmonスレッドがアイドル状態のP(プロセッサ)の数に基づいてパーク(一時停止)する現在の挙動に対し、ゴルーチンが実行中である限りsysmonスレッドがパークしないように変更を加えることで、ゴルーチンのプリエンプション(強制中断)の信頼性を向上させることを目的としています。
コミット
commit 156e8b306d009ef118a4138f34098c8c41976a08
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Jul 31 19:59:27 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=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/12167043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/156e8b306d009ef118a4138f34098c8c41976a08
元コミット内容
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ランタイムには、システムモニター(sysmon)と呼ばれる重要なバックグラウンドスレッドが存在します。このスレッドは、ガベージコレクションのトリガー、ネットワークポーリング、そして特に重要なゴルーチンのプリエンプションといった、ランタイムの健全性を維持するための様々なタスクを担当しています。
このコミット以前のGoランタイムでは、sysmonスレッドは、すべてのP(プロセッサ、論理CPU)がアイドル状態であると判断された場合(runtime.sched.npidle == runtime.gomaxprocs
)、自身をパーク(一時停止)させていました。これは、システムに実行すべきゴルーチンがない場合にCPUリソースを節約するための合理的な挙動でした。
しかし、sysmonスレッドがパーク状態から復帰する(アンパークされる)条件が限定的であるという問題がありました。以前は、ゴルーチンがシステムコール(syscall)に入ったときにのみsysmonスレッドがアンパークされていました。このメカニズムは、ブロッキングシステムコールに入ったゴルーチンからPを再取得する(他のゴルーチンにPを割り当てる)目的には十分でした。
しかし、このアンパーク条件では、ゴルーチンの「信頼性の高いプリエンプション」を実現するには不十分であることが判明しました。プリエンプションとは、長時間CPUを占有しているゴルーチンを強制的に中断し、他のゴルーチンにCPUを譲るメカニズムです。Go 1.2で非同期プリエンプションが導入されて以来、sysmonスレッドはSIGPROF
シグナルを送信することでゴルーチンをプリエンプトする役割を担っています。もしsysmonスレッドがパークしたままであれば、プリエンプションの機会が失われ、一部のゴルーチンがCPUを独占し、他のゴルーチンの実行が遅延する可能性があります。
このコミットの目的は、この問題を解決し、「いずれかのゴルーチンが実行中である限り、sysmonスレッドが確実に実行され続ける」ようにすることです。これにより、プリエンプションがより頻繁かつ確実に実行され、Goプログラム全体の応答性と公平なスケジューリングが向上します。
前提知識の解説
このコミットの変更内容を理解するためには、Goランタイムのスケジューラ、特にM, P, Gモデル、sysmonスレッド、ゴルーチンのプリエンプション、およびシステムコールに関する基本的な知識が必要です。
-
Goスケジューラ (M, P, Gモデル):
- G (Goroutine): Goにおける軽量な実行単位です。数百万個のゴルーチンを同時に実行できる設計になっています。各ゴルーチンは独自のスタックを持ち、Goランタイムによって管理されます。
- M (Machine): OSのスレッド(カーネルスレッド)を表します。MはGを実行するための実際の実行コンテキストを提供します。Goランタイムは必要に応じてMを生成・破棄します。
- P (Processor): 論理プロセッサを表します。MがGを実行するためにはPを保持している必要があります。Pの数は通常、
GOMAXPROCS
環境変数によって設定され、同時に実行できるゴルーチンの数を制限します。Pは、実行可能なゴルーチンをMに提供し、MがGを実行するためのリソース(例えば、ローカルな実行キュー)を管理します。 - M-P-Gの連携: MはPを介してGを実行します。Mがブロッキングシステムコールに入ると、そのMはPを解放し、他のMがそのPを使って別のGを実行できるようにします。これにより、OSスレッドのブロックがGoプログラム全体の並行性を阻害するのを防ぎます。
-
sysmon (System Monitor) スレッド:
- Goランタイムが起動すると、
sysmon
と呼ばれる特別なOSスレッドが起動します。このスレッドは、Goプログラムの実行中にバックグラウンドで様々な重要なタスクを周期的に実行します。 - 主な役割:
- プリエンプション: 長時間実行されているゴルーチンを強制的に中断し、他のゴルーチンにCPUを譲るためのシグナル(
SIGPROF
)を送信します。 - ネットワークポーリング: ネットワークI/Oの準備ができたことを監視し、ブロッキング中のゴルーチンを再開させます。
- ガベージコレクション (GC): GCのトリガー条件を監視し、必要に応じてGCを実行したり、GCヘルパーゴルーチンを起動したりします。
- デッドロック検出: すべてのゴルーチンがブロックされている状態(デッドロック)を検出し、プログラムを終了させます。
- Pの解放: ブロッキングシステムコールから戻ったMがPを再取得できない場合に、Pを解放します。
- プリエンプション: 長時間実行されているゴルーチンを強制的に中断し、他のゴルーチンにCPUを譲るためのシグナル(
- Goランタイムが起動すると、
-
Goroutineプリエンプション:
- Go 1.2以前は、ゴルーチンは協調的プリエンプション(cooperative preemption)のみをサポートしていました。これは、ゴルーチンが関数呼び出しやチャネル操作などの特定の「安全なポイント」で自発的にスケジューラに制御を返す必要があったことを意味します。無限ループのような計算集約的なゴルーチンは、自発的に制御を返さないため、他のゴルーチンの実行を妨げ、プログラム全体の応答性を低下させる可能性がありました。
- Go 1.2で非同期プリエンプション(asynchronous preemption)が導入されました。これにより、sysmonスレッドがOSのシグナル(Unix系OSでは
SIGPROF
)を利用して、ゴルーチンが自発的に制御を返さない場合でも強制的に中断できるようになりました。これにより、CPUを長時間占有するゴルーチンがあっても、他のゴルーチンが公平に実行されることが保証されます。
-
システムコール (syscall):
- GoプログラムがファイルI/O、ネットワーク通信、プロセス管理など、OSの機能を利用する際にはシステムコールが発行されます。
- システムコールには、すぐに結果が返る非ブロッキングなものと、I/Oの完了などを待つ間、OSスレッドが一時停止するブロッキングなものがあります。
- Goランタイムは、ゴルーチンがブロッキングシステムコールに入ると、そのゴルーチンを実行していたMからPを切り離し、そのPを他のMが利用できるようにします。システムコールが完了すると、ゴルーチンは再びPとMに割り当てられ、実行を再開します。
-
関連するランタイム変数と関数:
runtime.sched.npidle
: 現在アイドル状態にあるPの数を表す変数です。runtime.gomaxprocs
: Goランタイムが利用できるPの総数を表す変数です。runtime.atomicload(&runtime.sched.sysmonwait)
:runtime.sched.sysmonwait
変数の値をアトミックに読み込みます。アトミック操作は、複数のスレッドが同時に共有変数にアクセスする際に、データの整合性を保証するために使用されます。runtime.atomicstore(&runtime.sched.sysmonwait, 0)
:runtime.sched.sysmonwait
変数の値をアトミックに0
に設定します。runtime.notewakeup(&runtime.sched.sysmonnote)
:runtime.sched.sysmonnote
というNote
オブジェクトに関連付けられたスレッド(この場合はsysmonスレッド)をウェイクアップ(スリープ状態から復帰)させます。Note
はGoランタイム内部でスレッド間の同期に使用されるプリミティブです。
技術的詳細
このコミットの核心は、sysmonスレッドのアンパーク条件を拡張し、ゴルーチンのプリエンプションをより確実にすることにあります。
以前の挙動では、sysmonスレッドは、すべてのPがアイドル状態(runtime.sched.npidle == runtime.gomaxprocs
)になるとパークしていました。そして、ゴルーチンがシステムコールに入ったときにのみアンパークされていました。このアンパーク条件は、ブロッキングシステムコールからPを再利用する目的には十分でしたが、プリエンプションの目的には不十分でした。なぜなら、システムコールに入らない計算集約的なゴルーチンが長時間実行されている場合、sysmonスレッドがパークしたままであれば、そのゴルーチンをプリエンプトする機会が失われるからです。
このコミットでは、exitsyscallfast
とexitsyscall0
という、ゴルーチンがシステムコールから戻る際に呼び出される関数に重要な変更が加えられました。これらの関数内で、ゴルーチンがシステムコールから戻り、Pが利用可能になった際に、sysmonスレッドが待機状態(runtime.sched.sysmonwait
フラグがセットされている状態)であれば、明示的にsysmonスレッドをアンパークするロジックが追加されました。
具体的には、以下の条件がチェックされます。
p
: システムコールから戻ったゴルーチンがP(プロセッサ)を正常に取得できたか、またはPが利用可能になったか。runtime.atomicload(&runtime.sched.sysmonwait)
: sysmonスレッドが現在、ウェイクアップを待機している状態であるか。
もしこれらの条件が両方とも真であれば、つまり、システムコールから戻ったゴルーチンによってPが利用可能になり、かつsysmonスレッドがウェイクアップを待機している状態であれば、以下の処理が行われます。
runtime.atomicstore(&runtime.sched.sysmonwait, 0)
:sysmonwait
フラグをクリアし、sysmonスレッドがウェイクアップされたことを示します。runtime.notewakeup(&runtime.sched.sysmonnote)
: sysmonスレッドをウェイクアップします。
この変更により、システムコールから戻るゴルーチンがPを解放または取得するたびに、sysmonスレッドがウェイクアップされる可能性が高まります。これにより、たとえすべてのPが一時的にアイドル状態になったとしても、ゴルーチンがシステムコールから戻ることでsysmonスレッドがすぐに再開され、プリエンプションを含むその役割を果たすことができるようになります。結果として、Goランタイムは、実行中のゴルーチンが存在する限り、より積極的にプリエンプションを試みることが可能になり、プログラム全体の応答性と公平性が向上します。
また、runtime·sigprof
関数(プロファイリングのためにSIGPROF
シグナルを処理する関数)にも変更が加えられています。これは、特定の状況下(例えば、g0
やシグナルハンドラゴルーチンのプロファイリング、またはレース検出器がアクティブな場合)では、完全なスタックトレースバックを行わないようにするためのものです。これは、これらの特殊なコンテキストでのスタックトレースバックが、デッドロックを引き起こしたり、不正確な情報を提供したりする可能性があるためと考えられます。この部分はsysmonのアンパークとは直接関係ありませんが、同じコミットでランタイムの安定性と正確性を向上させるための変更として含まれています。
コアとなるコードの変更箇所
変更は主に src/pkg/runtime/proc.c
ファイルにあります。
-
exitsyscallfast
関数内:--- 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);
-
exitsyscall0
関数内:--- a/src/pkg/runtime/proc.c +++ b/src/pkg/runtime/proc.c @@ -1559,6 +1563,10 @@ exitsyscall0(G *gp)\n \tp = pidleget();\n \tif(p == nil)\n \t\tglobrunqput(gp);\n+\telse if(runtime·atomicload(&runtime·sched.sysmonwait)) {\n+\t\truntime·atomicstore(&runtime·sched.sysmonwait, 0);\n+\t\truntime·notewakeup(&runtime·sched.sysmonnote);\n+\t}\n \truntime·unlock(&runtime·sched);\n \tif(p) {\n \t\tacquirep(p);\
-
runtime·sigprof
関数内:--- a/src/pkg/runtime/proc.c +++ b/src/pkg/runtime/proc.c @@ -1924,24 +1932,38 @@ static struct {\n \tuintptr pcbuf[100];\n } prof;\n \n+static void\n+System(void)\n+{\n+}\n+\n // Called if we receive a SIGPROF signal.\n void\n runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp)\n {\n \tint32 n;\n+\tbool traceback;\n \n-\t// Windows does profiling in a dedicated thread w/o m.\n-\tif(!Windows && (m == nil || m->mcache == nil))\n-\t\treturn;\n \tif(prof.fn == nil || prof.hz == 0)\n \t\treturn;\n+\ttraceback = true;\n+\t// Windows does profiling in a dedicated thread w/o m.\n+\tif(!Windows && (m == nil || m->mcache == nil))\n+\t\ttraceback = false;\n+\tif(gp == m->g0 || gp == m->gsignal)\n+\t\ttraceback = false;\n+\tif(m != nil && m->racecall)\n+\t\ttraceback = false;\n \n \truntime·lock(&prof);\n \tif(prof.fn == nil) {\n \t\truntime·unlock(&prof);\n \t\treturn;\n \t}\n-\tn = runtime·gentraceback((uintptr)pc, (uintptr)sp, (uintptr)lr, gp, 0, prof.pcbuf, nelem(prof.pcbuf), nil, nil, false);\n+\tn = 1;\n+\tprof.pcbuf[0] = (uintptr)pc;\n+\tif(traceback)\n+\t\tn = runtime·gentraceback((uintptr)pc, (uintptr)sp, (uintptr)lr, gp, 0, prof.pcbuf, nelem(prof.pcbuf), nil, nil, false);\ \tif(n > 0)\n \tprof.fn(prof.pcbuf, n);\ \truntime·unlock(&prof);\
コアとなるコードの解説
-
exitsyscallfast
およびexitsyscall0
における変更:- これらの関数は、ゴルーチンがシステムコールから戻る際に呼び出されます。
exitsyscallfast
は高速パス、exitsyscall0
はより一般的なパスです。 - 追加されたコードブロック:
if(p && runtime·atomicload(&runtime·sched.sysmonwait)) { runtime·atomicstore(&runtime·sched.sysmonwait, 0); runtime·notewakeup(&runtime·sched.sysmonnote); }
p
: システムコールから戻ったゴルーチンがP(プロセッサ)を正常に取得できた場合、またはPが利用可能になった場合に真となります。runtime·atomicload(&runtime·sched.sysmonwait)
:sysmonwait
フラグがセットされているか(sysmonスレッドがウェイクアップを待機しているか)をアトミックに確認します。- この
if
文の条件が満たされた場合、つまり、Pが利用可能になり、かつsysmonスレッドが待機している場合、runtime·atomicstore(&runtime·sched.sysmonwait, 0)
でsysmonwait
フラグをクリアし、runtime·notewakeup(&runtime·sched.sysmonnote)
でsysmonスレッドをウェイクアップします。
- この変更により、システムコールから戻るゴルーチンがPを解放または取得するたびに、sysmonスレッドがウェイクアップされる可能性が高まります。これにより、sysmonスレッドは、たとえすべてのPが一時的にアイドル状態になったとしても、ゴルーチンがシステムコールから戻ることで直ちに再開され、プリエンプションを含むその役割を果たすことができるようになります。
- これらの関数は、ゴルーチンがシステムコールから戻る際に呼び出されます。
-
runtime·sigprof
における変更:runtime·sigprof
は、プロファイリングのためにSIGPROF
シグナルを受信した際に呼び出される関数です。traceback
という新しいbool
型変数が導入されました。- 以前は、特定の条件(Windows以外でMがnilまたはmcacheがnilの場合)で早期リターンしていましたが、このコミットでは
traceback = false
を設定する条件として拡張されました。 gp == m->g0 || gp == m->gsignal
:g0
(スケジューラ自身のゴルーチン)やシグナルハンドラゴルーチンをプロファイリングしている場合、traceback
をfalse
に設定します。これらのゴルーチンは特殊なコンテキストで実行されるため、通常のスタックトレースバックが適切でない場合があります。m != nil && m->racecall
: レース検出器がアクティブな場合もtraceback
をfalse
に設定します。runtime·gentraceback
(スタックトレースバックを生成する関数)の呼び出しがtraceback
変数の値に依存するようになりました。traceback
がfalse
の場合、n
は1
に設定され、prof.pcbuf[0]
には現在のPC(プログラムカウンタ)のみが格納され、完全なスタックトレースバックは行われません。- この変更は、特定のランタイム内部のゴルーチンや特殊な状況下でのプロファイリングにおいて、不正確なスタックトレースバックを避け、ランタイムの安定性を向上させるためのものです。sysmonのアンパークとは直接的な関連はありませんが、ランタイムの堅牢性を高めるための改善として同じコミットに含まれています。
関連リンク
- Go Scheduler: M, P, G in Golang: https://medium.com/golang-learn/go-scheduler-m-p-g-in-golang-79776201d06c
- Go's execution tracer: https://go.dev/blog/go15trace (sysmonの活動もトレースされる)
- Go runtime preemption: https://go.dev/blog/go14-runtime (Go 1.4でのプリエンプションの改善について触れているが、Go 1.2で非同期プリエンプションが導入された背景を理解するのに役立つ)
参考にした情報源リンク
- GitHub Commit: https://github.com/golang/go/commit/156e8b306d009ef118a4138f34098c8c41976a08
- Go CL 12167043: https://golang.org/cl/12167043 (コミットメッセージに記載されている変更リストへのリンク)
- Goのソースコード (
src/pkg/runtime/proc.c
) - Goのスケジューラに関する一般的な知識とドキュメント
- Goのプリエンプションに関する一般的な知識とドキュメント
- アトミック操作に関する一般的な知識