[インデックス 13855] ファイルの概要
コミット
このコミットは、Goランタイムにおけるゴルーチンのブロッキングメカニズムをリファクタリングするものです。特に、新しいスケジューラの導入に向けた準備作業として、runtime.park()
関数を導入し、既存のゴルーチン待機処理をこの新しい関数に置き換えています。これにより、ミューテックスのアンロックとゴルーチンのパーク(待機状態への移行)をアトミックに行えるようになり、既存の競合状態を引き起こしやすい readyonstop
フラグの利用を排除することを目指しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f20fd87384d152ed91439e824333fbb78688e741
元コミット内容
commit f20fd87384d152ed91439e824333fbb78688e741
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Sep 18 21:15:46 2012 +0400
runtime: refactor goroutine blocking
The change is a preparation for the new scheduler.
It introduces runtime.park() function,
that will atomically unlock the mutex and park the goroutine.
It will allow to remove the racy readyonstop flag
that is difficult to implement w/o the global scheduler mutex.
R=rsc, remyoudompheng, dave
CC=golang-dev
https://golang.org/cl/6501077
変更の背景
この変更の主な背景は、Goランタイムにおける新しいスケジューラの導入準備です。当時のGoスケジューラは、特定のシナリオで競合状態(race condition)を引き起こす可能性のある readyonstop
フラグに依存していました。このフラグは、グローバルなスケジューラミューテックスなしでは正しく実装することが困難であり、パフォーマンスのボトルネックや複雑性の原因となっていました。
新しいスケジューラは、より効率的でスケーラブルなゴルーチン管理を目指しており、そのためには既存のブロッキングメカニズムの根本的な改善が必要でした。特に、ミューテックスのアンロックとゴルーチンの待機状態への移行という二つの操作をアトミックに実行できるメカニズムが求められていました。これにより、readyonstop
フラグのような競合状態を引き起こしやすい設計を排除し、より堅牢でシンプルなスケジューラの実装を可能にすることが目的です。
前提知識の解説
Goランタイムとゴルーチン
Goランタイムは、Goプログラムの実行を管理するシステムです。その中心的な概念の一つが「ゴルーチン(goroutine)」です。ゴルーチンは軽量な並行実行単位であり、OSのスレッドよりもはるかに少ないリソースで作成・管理されます。数千、数万のゴルーチンを同時に実行することがGoの大きな特徴です。
Goスケジューラ
Goスケジューラは、多数のゴルーチンを限られた数のOSスレッドに効率的にマッピングし、実行を管理する役割を担っています。ゴルーチンがI/O操作やチャネル操作などでブロックされる(待機状態になる)と、スケジューラはそのゴルーチンを一時停止させ、別の実行可能なゴルーチンにCPUを割り当てます。これにより、CPUリソースを最大限に活用し、高い並行性を実現します。
ゴルーチンの状態
ゴルーチンは、そのライフサイクルにおいて様々な状態を取ります。このコミットで特に重要なのは以下の状態です。
- Grunning: ゴルーチンが現在CPU上で実行されている状態。
- Gwaiting: ゴルーチンが何らかのイベント(I/O完了、チャネルからのデータ受信、タイマー期限切れなど)を待ってブロックされている状態。この状態のゴルーチンはCPUを消費しません。
ミューテックスとロック
ミューテックス(Mutex)は、複数のゴルーチンが共有リソースに同時にアクセスするのを防ぐための同期プリミティブです。ミューテックスを「ロック」することで、そのリソースへの排他的アクセスを保証し、データ競合(data race)を防ぎます。リソースの使用が終わったら、ミューテックスを「アンロック」して他のゴルーチンがアクセスできるようにします。
競合状態 (Race Condition)
競合状態とは、複数の並行プロセスやスレッド(この場合はゴルーチン)が共有リソースにアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。これはプログラムのバグの一般的な原因であり、デバッグが非常に困難です。
runtime.gosched()
runtime.gosched()
は、現在のゴルーチンを一時停止させ、スケジューラに制御を戻す関数です。これにより、スケジューラは別の実行可能なゴルーチンを選択し、実行を開始できます。これは協調的マルチタスクの一種であり、ゴルーチンが自発的にCPUを明け渡すことで、他のゴルーチンに実行機会を与えます。
readyonstop
フラグ (推測)
コミットメッセージに登場する racy readyonstop flag
は、当時のGoランタイム内部で使われていた、ゴルーチンが停止(ブロック)した際に、そのゴルーチンをすぐに実行可能状態(ready)に戻すべきかどうかを示すフラグであると推測されます。このフラグの管理が、グローバルなスケジューラミューテックスなしでは競合状態を引き起こしやすかった、と説明されています。これは、ゴルーチンがブロックされる直前と、そのゴルーチンを再開させるイベントが発生するタイミングとの間に、微妙な競合ウィンドウが存在したことを示唆しています。
技術的詳細
このコミットの核心は、runtime.park()
という新しいランタイム関数の導入とその適用です。
runtime.park()
の役割
runtime.park()
関数は、ゴルーチンを待機状態(Gwaiting
)に移行させ、同時にオプションで指定されたミューテックスをアトミックにアンロックする機能を提供します。そのシグネチャは以下の通りです。
void runtime·park(void (*unlockf)(Lock*), Lock *lock, int8 *reason)
unlockf
: ゴルーチンをパークする前に実行されるアンロック関数へのポインタです。通常、これはruntime·unlock
のようなミューテックスアンロック関数になります。lock
:unlockf
に渡されるミューテックスへのポインタです。reason
: ゴルーチンが待機する理由を示す文字列です。デバッグやプロファイリングに役立ちます。
この関数が実行されると、以下のステップが順に実行されます。
- 現在のゴルーチン
g
の状態をGwaiting
に設定します。 - 待機理由
reason
をg->waitreason
に設定します。 unlockf
が指定されていれば、lock
を引数としてunlockf
を呼び出し、ミューテックスをアンロックします。このステップが、ミューテックスのアンロックとゴルーチンのパークをアトミックに行う上で非常に重要です。runtime·gosched()
を呼び出し、スケジューラに制御を戻します。これにより、現在のゴルーチンは実行を中断し、別のゴルーチンが実行される機会を得ます。
アトミックな操作の重要性
runtime.park()
がミューテックスのアンロックとゴルーチンのパークをアトミックに行うことの重要性は、競合状態の回避にあります。
例えば、チャネル操作において、ゴルーチンがチャネルのロックを保持したまま待機状態に入ろうとする場合を考えます。もし「ロックをアンロックする」と「ゴルーチンを待機状態にする」という操作が別々に行われると、その間に別のゴルーチンがチャネルにアクセスしようとしてデッドロックに陥ったり、不正な状態に遭遇したりする可能性があります。
具体的には、従来のコードでは以下のようなパターンが見られました。
g->status = Gwaiting;
g->waitreason = "...";
runtime·unlock(c); // ここでロックを解放
runtime·gosched(); // ここでスケジューラに制御を戻し、ゴルーチンが待機状態になる
この runtime·unlock(c);
と runtime·gosched();
の間に、別のゴルーチンが割り込んでチャネル c
にアクセスし、問題を引き起こす可能性がありました。runtime.park()
はこの2つの操作を単一の関数呼び出しにまとめることで、この競合ウィンドウを排除します。
readyonstop
フラグの排除
runtime.park()
の導入により、ゴルーチンがブロックされる際に、そのゴルーチンを再開させるイベントが既に発生しているかどうかを判断するために使われていた readyonstop
フラグの必要性がなくなります。park
関数は、ゴルーチンが待機状態に入る直前にミューテックスを解放するため、イベントが既に発生している場合は、park
から戻った直後に runtime.ready()
によってゴルーチンが即座に実行可能状態に戻されることが保証されます。これにより、複雑なフラグ管理やそれに伴う競合状態が解消されます。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
src/pkg/runtime/chan.c
: チャネルの送受信操作におけるゴルーチンのブロッキングロジックがruntime.park()
を使用するように変更されました。- 例:
runtime·unlock(c); runtime·gosched();
がruntime·park(runtime·unlock, c, "chan send");
に置き換えられています。
- 例:
src/pkg/runtime/mgc0.c
: ガベージコレクションのファイナライザ待機ロジックがruntime.park()
を使用するように変更されました。src/pkg/runtime/proc.c
: 新しい関数runtime.park()
の定義が追加されました。src/pkg/runtime/runtime.h
:runtime.park()
の関数プロトタイプが追加され、runtime.tsleep
のシグネチャが変更されました。src/pkg/runtime/sema.goc
: セマフォの取得操作におけるブロッキングロジックがruntime.park()
を使用するように変更されました。src/pkg/runtime/time.goc
: タイマー関連の関数(特にruntime.tsleep
)におけるゴルーチンのスリープロジックがruntime.park()
を使用するように変更されました。
コアとなるコードの解説
最も重要な変更は src/pkg/runtime/proc.c
に追加された runtime.park()
関数の実装です。
// src/pkg/runtime/proc.c
// Puts the current goroutine into a waiting state and unlocks the lock.
// The goroutine can be made runnable again by calling runtime·ready(gp).
void
runtime·park(void (*unlockf)(Lock*), Lock *lock, int8 *reason)
{
g->status = Gwaiting; // 現在のゴルーチンの状態をGwaitingに設定
g->waitreason = reason; // 待機理由を設定
if(unlockf) // アンロック関数が指定されていれば
unlockf(lock); // ロックをアンロック
runtime·gosched(); // スケジューラに制御を戻し、現在のゴルーチンを一時停止
}
この関数は、ゴルーチンを待機状態に移行させる際に、必要に応じてミューテックスをアンロックする処理を統合しています。これにより、ミューテックスのアンロックとゴルーチンのパークの間に他のゴルーチンが割り込むことによる競合状態を防ぎます。
他のファイルでは、既存のゴルーチンを待機させるための複数の行(g->status = Gwaiting; g->waitreason = "..."; runtime·unlock(c); runtime·gosched();
のようなパターン)が、単一の runtime·park()
呼び出しに置き換えられています。これにより、コードの重複が減り、ブロッキングロジックが一元化され、将来のスケジューラ変更への対応が容易になります。
例えば、src/pkg/runtime/chan.c
の runtime·chansend
関数では、チャネル送信時にゴルーチンがブロックされる箇所が以下のように変更されています。
変更前:
// src/pkg/runtime/chan.c (抜粋)
// ...
g->status = Gwaiting;
g->waitreason = "chan send";
enqueue(&c->sendq, &mysg);
runtime·unlock(c); // チャネルのロックをアンロック
runtime·gosched(); // ゴルーチンを一時停止
// ...
変更後:
// src/pkg/runtime/chan.c (抜粋)
// ...
enqueue(&c->sendq, &mysg);
runtime·park(runtime·unlock, c, "chan send"); // ロックのアンロックとゴルーチンのパークをアトミックに実行
// ...
この変更により、チャネルのロック c
をアンロックし、同時に現在のゴルーチンを「chan send」という理由で待機状態にする処理が、より安全かつ簡潔に記述されています。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/
- Goランタイムスケジューラに関する一般的な情報: https://go.dev/doc/effective_go#concurrency (一般的な並行処理の概念)
参考にした情報源リンク
- Goのコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている
https://golang.org/cl/6501077
は、当時のGerritのURL形式です。現在はgo-review.googlesource.com/c/go/+/6501077
のような形式にリダイレクトされる可能性がありますが、直接アクセスは難しいかもしれません。) - Goランタイムの内部構造に関する一般的な知識 (書籍やブログ記事など)