[インデックス 16928] ファイルの概要
このコミットは、Goランタイムにおけるゴルーチンプリエンプション(横取り)機能を有効にするものです。コミットメッセージによると、プリエンプションに関する既知の問題がすべて修正されたため、この機能を有効にすることが可能になりました。これにより、Goプログラムのスケジューリングの公平性が向上し、長時間実行されるゴルーチンが他のゴルーチンの実行を妨げることがなくなります。
コミット
- コミットハッシュ:
a20784bdafc9a594a2c70be1e91f4b86182e6a21
- Author: Dmitriy Vyukov dvyukov@google.com
- Date: Tue Jul 30 22:17:38 2013 +0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a20784bdafc9a594a2c70be1e91f4b86182e6a21
元コミット内容
runtime: enable goroutine preemption
All known issues with preemption have beed fixed.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/12008044
変更の背景
Goランタイムは、複数のゴルーチンを効率的にスケジューリングし、並行処理を実現します。しかし、初期のGoランタイムでは、ゴルーチンのプリエンプション(横取り)が完全には実装されていませんでした。特に、関数呼び出しを伴わない計算集約的なループを持つゴルーチンは、自ら実行を中断しない限り、CPUを占有し続ける可能性がありました。これは、他のゴルーチンが実行される機会を奪い、プログラム全体の応答性や公平性を損なう原因となっていました。
このコミットが行われた2013年頃は、Goランタイムのスケジューラが活発に開発・改善されていた時期です。プリエンプションは、スケジューラの公平性と効率性を高める上で非常に重要な機能であり、その実装には多くの技術的課題が伴いました。例えば、プリエンプションポイントの特定、スタックの安全な巻き戻し、GCとの連携などが挙げられます。
このコミットは、「プリエンプションに関する既知の問題がすべて修正された」というメッセージが示す通り、それまでの開発努力が実を結び、プリエンプション機能が安定して動作するようになったことを意味します。これにより、Goプログラムはより予測可能で、公平なスケジューリング動作を示すようになります。
前提知識の解説
ゴルーチン (Goroutine)
Goにおける軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行することも可能です。Goランタイムがゴルーチンのスケジューリング、スタックの管理、通信(チャネル)などを担当します。
Goスケジューラ (Go Scheduler)
Goランタイムの重要なコンポーネントで、ゴルーチンをOSスレッド(M: Machine)にマッピングし、実行を管理します。Goスケジューラは、GPMモデル(Goroutine, Processor, Machine)に基づいて動作します。
- G (Goroutine): 実行されるゴルーチン。
- M (Machine): OSスレッド。ゴルーチンを実行する実際のOSスレッド。
- P (Processor): 論理プロセッサ。Mがゴルーチンを実行するために必要なコンテキスト。Pは実行可能なゴルーチンのキューを持ち、MはPにアタッチされてゴルーチンを実行します。
プリエンプション (Preemption)
プリエンプションとは、実行中のタスク(この場合はゴルーチン)が、自発的に実行を中断することなく、外部からの介入によって強制的に実行を中断させられ、他のタスクにCPUの実行権が渡される仕組みです。これにより、特定のタスクがCPUを独占することを防ぎ、システム全体の応答性や公平性を保つことができます。
Goにおけるプリエンプションは、主に以下の2種類があります。
-
協調的プリエンプション (Cooperative Preemption): Goの初期バージョンで採用されていた方式です。ゴルーチンは、関数呼び出し(特にランタイム関数呼び出し)の際に、自発的にスケジューラに制御を戻す機会を提供します。これにより、スケジューラは他のゴルーチンに切り替えることができます。しかし、関数呼び出しを含まない計算集約的なループ(例:
for {}
)は、この機会を提供しないため、CPUを独占し続ける問題がありました。 -
非同期プリエンプション (Asynchronous Preemption): このコミットで有効化されたプリエンプションは、非同期プリエンプションの一種です。これは、シグナル(Unix系OSの場合)などのOSのメカニズムを利用して、実行中のゴルーチンを強制的に中断させるものです。Goランタイムは、タイマーを設定し、一定時間(例えば10ms)が経過してもゴルーチンが実行を継続している場合、そのゴルーチンに対してプリエンプションシグナルを送信します。シグナルを受け取ったゴルーチンは、安全なポイントで実行を中断し、スケジューラに制御を戻します。これにより、関数呼び出しがない計算集約的なゴルーチンも公平にプリエンプトされるようになります。
proc.c
Goランタイムのスケジューラやゴルーチン管理に関するC言語で書かれたコードが含まれるファイルです。Goランタイムの低レベルな部分を担っています。
技術的詳細
このコミットの核心は、Goランタイムにおけるゴルーチンプリエンプションの有効化です。具体的には、src/pkg/runtime/proc.c
内の preemptone
関数における条件分岐の変更と、src/pkg/runtime/proc_test.go
内のテストの変更です。
preemptone
関数
preemptone
関数は、特定のP(論理プロセッサ)上で実行されているゴルーチンをプリエンプト(横取り)しようとするランタイム内部の関数です。この関数は、スケジューラがゴルーチンの実行時間を制限し、他のゴルーチンにCPUを割り当てるために使用されます。
コミット前のコードでは、preemptone
関数は常に return
していました。これは、プリエンプション機能がまだ開発中または不安定であったため、一時的に無効化されていたことを意味します。
// For now, disable.
// The if(1) silences a compiler warning about the rest of the
// function being unreachable.
if(1) return; // <-- ここが変更点
if(1) return;
は、コンパイラに対して「この関数は常にここでリターンする」と伝え、それ以降のコードが到達不能であるという警告を抑制するための慣用的な記述です。つまり、この関数は実質的に何もせず、プリエンプションは行われませんでした。
このコミットでは、この行が if(0) return;
に変更されました。
if(0) return; // <-- 変更後
if(0)
は常に偽であるため、return
文は実行されなくなり、preemptone
関数の本来のロジックが実行されるようになります。これにより、ランタイムはゴルーチンを積極的にプリエンプトするようになります。
proc_test.go
の変更
src/pkg/runtime/proc_test.go
は、Goランタイムのプロセッサ(スケジューラ)関連の機能をテストするためのファイルです。このコミットでは、TestPreemption
と TestPreemptionGC
という2つのテスト関数から t.Skip("preemption is disabled")
の行が削除されています。
func TestPreemption(t *testing.T) {
// t.Skip("preemption is disabled") // <-- 削除
// Test that goroutines are preempted at function calls.
// ...
}
func TestPreemptionGC(t *testing.T) {
// t.Skip("preemption is disabled") // <-- 削除
// Test that pending GC preempts running goroutines.
// ...
}
これは、プリエンプション機能が有効になったため、これらのテストをスキップする必要がなくなったことを意味します。これらのテストは、ゴルーチンが関数呼び出し時やGC(ガベージコレクション)の際に正しくプリエンプトされることを検証するものです。テストが実行されるようになったことで、プリエンプション機能が期待通りに動作していることが確認されます。
プリエンプションのメカニズム(補足)
Goランタイムの非同期プリエンプションは、主に以下のメカニズムで実現されます。
- タイマーとシグナル: Goランタイムは、各P(論理プロセッサ)に対してタイマーを設定します。このタイマーは、ゴルーチンが一定時間(例えば10ms)以上実行され続けている場合に発火します。タイマーが発火すると、ランタイムはOSに対してシグナル(Unix系OSでは
SIGURG
など)を送信し、現在実行中のOSスレッド(M)に割り込みをかけます。 - シグナルハンドラ: シグナルを受け取ったOSスレッドは、Goランタイムが設定したシグナルハンドラを実行します。このハンドラ内で、ランタイムは現在実行中のゴルーチンのスタックを検査し、安全にプリエンプトできるポイント(例えば、関数プロローグやループのバックエッジなど)を探します。
- スタックの書き換え: 安全なポイントが見つかると、ランタイムはゴルーチンのスタックを書き換え、スケジューラに制御を戻すためのコード(
runtime.morestack
など)を挿入します。 - スケジューラの再実行: ゴルーチンは、次に実行される際に、挿入されたコードによってスケジューラに制御を戻し、スケジューラは他の実行可能なゴルーチンに切り替えます。
このコミットは、この複雑なプリエンプションメカニズムが十分に安定し、本番環境で有効にできるレベルに達したことを示しています。
コアとなるコードの変更箇所
diff --git a/src/pkg/runtime/proc.c b/src/pkg/runtime/proc.c
index 0f44f6b981..c4b8c02517 100644
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -2266,7 +2266,7 @@ preemptone(P *p)
// For now, disable.
// The if(1) silences a compiler warning about the rest of the
// function being unreachable.
-if(1) return;
+if(0) return;
mp = p->m;
if(mp == nil || mp == m)
diff --git a/src/pkg/runtime/proc_test.go b/src/pkg/runtime/proc_test.go
index 100deb8f23..8f47553fb4 100644
--- a/src/pkg/runtime/proc_test.go
+++ b/src/pkg/runtime/proc_test.go
@@ -193,7 +193,6 @@ var preempt = func() int {
}
func TestPreemption(t *testing.T) {
- t.Skip("preemption is disabled")
// Test that goroutines are preempted at function calls.
const N = 5
c := make(chan bool)
@@ -214,7 +213,6 @@ func TestPreemption(t *testing.T) {
}
func TestPreemptionGC(t *testing.T) {
- t.Skip("preemption is disabled")
// Test that pending GC preempts running goroutines.
const P = 5
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(P + 1))
コアとなるコードの解説
src/pkg/runtime/proc.c
の変更
proc.c
ファイル内の preemptone
関数において、以下の行が変更されました。
-if(1) return;
+if(0) return;
この変更は、preemptone
関数の実行フローを根本的に変えます。
- 変更前 (
if(1) return;
):if(1)
は常に真であるため、preemptone
関数は常に即座にリターンし、その後のプリエンプションロジックは実行されませんでした。これは、プリエンプション機能が意図的に無効化されていた状態を示します。 - 変更後 (
if(0) return;
):if(0)
は常に偽であるため、return
文は実行されず、preemptone
関数の残りのコード(ゴルーチンをプリエンプトするための実際のロジック)が実行されるようになります。これにより、Goランタイムはゴルーチンを積極的にプリエンプトする機能が有効になります。
src/pkg/runtime/proc_test.go
の変更
proc_test.go
ファイル内の TestPreemption
および TestPreemptionGC
関数から、以下の行が削除されました。
- t.Skip("preemption is disabled")
t.Skip()
は、Goのテストフレームワークにおいて、特定のテストをスキップするために使用される関数です。
- 変更前: これらのテストは、プリエンプション機能が無効化されていたため、実行しても意味がないか、失敗する可能性があったためスキップされていました。
- 変更後: プリエンプション機能が有効になったため、これらのテストをスキップする必要がなくなりました。これにより、Goランタイムのプリエンプション機能が正しく動作していることを自動テストで検証できるようになります。
TestPreemption
は関数呼び出しにおけるプリエンプションを、TestPreemptionGC
はGCが保留中の場合のプリエンプションをテストします。
これらの変更は、Goランタイムのプリエンプション機能が開発段階から安定版へと移行し、本番環境での利用が可能になったことを明確に示しています。これにより、Goプログラムはより公平で応答性の高いスケジューリング動作を示すようになります。
関連リンク
- Go CL 12008044: https://golang.org/cl/12008044
参考にした情報源リンク
- Go Scheduler: M, P, G in Golang: https://medium.com/@ankur_anand/go-scheduler-m-p-g-in-golang-7979209a06fe
- Go's preemptive scheduler: https://www.ardanlabs.com/blog/2018/12/goroutine-preemption.html
- Go: Asynchronous Preemption: https://go.dev/blog/go1.14-preemption (このコミットはGo 1.14より前のものですが、非同期プリエンプションの概念を理解する上で参考になります)
- The Go scheduler: https://go.dev/doc/articles/go_scheduler.html
- Understanding Goroutine Preemption in Go: https://www.geeksforgeeks.org/understanding-goroutine-preemption-in-go/
- Go runtime source code (proc.c): https://github.com/golang/go/blob/master/src/runtime/proc.go (現在の
proc.go
はGoで書かれていますが、当時のproc.c
の役割を理解する上で参考になります) - Go runtime source code (proc_test.go): https://github.com/golang/go/blob/master/src/runtime/proc_test.go
- Go issue tracker (related issues for preemption): https://github.com/golang/go/issues?q=is%3Aissue+preemption
- Go mailing list archives (discussions on preemption): https://groups.google.com/g/golang-dev (検索機能で"preemption"を検索)
- Go 1.2 Release Notes (preemption was a major feature in Go 1.2): https://go.dev/doc/go1.2 (このコミットはGo 1.2リリース前後のものと推測されます)
- Go 1.2 runtime changes: https://go.dev/doc/go1.2#runtime