[インデックス 17813] ファイルの概要
このコミットは、Goランタイムのsrc/pkg/runtime/proc.c
ファイルにおけるネットワークポーリングの条件判定の誤りを修正するものです。具体的には、sysmon
ゴルーチン内でネットワークをポーリングすべきタイミングを判断するロジックの比較演算子が誤っていた点を修正しています。
コミット
commit 88c448ba40a29ac96563e9945d2af17fba779d23
Author: Ian Lance Taylor <iant@golang.org>
Date: Thu Oct 17 11:57:48 2013 -0700
runtime: correct test for when to poll network
Fixes #6610.
R=golang-dev, khr
CC=golang-dev
https://golang.org/cl/14793043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/88c448ba40a29ac96563e9945d2af17fba779d23
元コミット内容
runtime: correct test for when to poll network
このコミットは、Goランタイムがネットワークをポーリングするタイミングを決定するテスト(条件判定)を修正します。
Fixes #6610.
これは、GoのIssueトラッカーにおけるIssue 6610を修正するものです。
変更の背景
Goランタイムには、バックグラウンドで様々なタスクを実行するsysmon
(System Monitor)と呼ばれる特別なゴルーチンが存在します。このsysmon
ゴルーチンは、ガベージコレクションのトリガー、スケジューラのポーリング、そしてネットワークI/Oのポーリングなど、ランタイムの健全性を維持するための重要な役割を担っています。
ネットワークI/Oのポーリングは、ノンブロッキングI/O操作が完了したかどうかを確認し、それらを待機しているゴルーチンを再開するために不可欠です。sysmon
は、ネットワークポーリングが最後に実行されてから一定時間(この場合は10ミリ秒)が経過した場合にのみ、再度ポーリングを実行するように設計されていました。これは、ポーリングの頻度を制限し、システムリソースの無駄な消費を防ぐための最適化です。
しかし、元のコードでは、この「10ミリ秒以上経過したか」という条件を判定するロジックに誤りがありました。具体的には、lastpoll + 10*1000*1000 > now
という条件が使用されており、これは「最後にポーリングしてから10ミリ秒が経過していない場合」に真となる条件でした。つまり、ポーリングが必要ない場合にポーリングを実行しようとする、あるいはポーリングが必要な場合に実行しないという逆の動作をしてしまうバグが存在していました。このバグは、ネットワークI/Oの遅延や、場合によってはデッドロックのような問題を引き起こす可能性がありました。
この問題はIssue #6610として報告され、このコミットによって修正されました。
前提知識の解説
Goランタイムとスケジューラ
Goプログラムは、Goランタイムによって管理されます。Goランタイムは、ゴルーチン(軽量スレッド)のスケジューリング、メモリ管理(ガベージコレクション)、チャネル通信、ネットワークI/Oなど、プログラムの実行に必要な低レベルの機能を提供します。
Goスケジューラは、M:Nスケジューリングモデルを採用しています。これは、M個のゴルーチンをN個のOSスレッドにマッピングするものです。スケジューラは、ゴルーチンを効率的にOSスレッド上で実行し、I/Oブロッキングなどの状況でゴルーチンを切り替えることで、高い並行性を実現します。
sysmon
ゴルーチン
sysmon
(System Monitor)は、Goランタイムが起動すると同時にバックグラウンドで実行される特別なゴルーチンです。このゴルーチンは、Goプログラムの健全な動作を維持するために、以下のような重要なタスクを周期的に実行します。
- ネットワークポーリング: ノンブロッキングネットワークI/O操作の完了を監視し、完了したI/Oを待機しているゴルーチンを再開します。
- タイマーの処理:
time.After
やtime.Sleep
などのタイマーイベントを処理します。 - プリエンプション: 長時間実行されているゴルーチンをプリエンプト(強制的に中断)し、他のゴルーチンにCPU時間を割り当てます。これにより、ゴルーチンの公平なスケジューリングが保証されます。
- ガベージコレクションのトリガー: 必要に応じてガベージコレクションをトリガーします。
- デッドロック検出: プログラムがデッドロック状態にあるかどうかを検出し、その場合はパニックを発生させます。
sysmon
は、これらのタスクを一定の間隔で実行しますが、特にネットワークポーリングに関しては、過度なポーリングを避けるために、前回のポーリングからの経過時間をチェックするロジックが含まれています。
runtime·atomicload64
とruntime·cas64
Goランタイムの内部では、アトミック操作が頻繁に使用されます。これは、複数のゴルーチンが共有データに同時にアクセスする際に、データの整合性を保つために不可欠です。
runtime·atomicload64(&runtime·sched.lastpoll)
: これは、runtime·sched.lastpoll
という64ビット変数の値をアトミックに読み込む関数です。アトミックな読み込みは、他のゴルーチンによる書き込み操作と競合することなく、変数の最新の値を確実に取得することを保証します。runtime·cas64(&runtime·sched.lastpoll, lastpoll, now)
: これは、Compare-And-Swap(CAS)操作を実行する関数です。runtime·sched.lastpoll
の現在の値がlastpoll
(読み込んだ古い値)と等しい場合にのみ、その値をnow
(新しい値)にアトミックに更新します。この操作は、複数のゴルーチンが同時に同じ変数を更新しようとした場合に、一貫性を保つために使用されます。更新が成功した場合にのみ、そのゴルーチンが値を変更したことになります。
runtime·nanotime()
runtime·nanotime()
は、Goランタイムが提供する高精度な時間測定関数です。これは、システム起動時からの経過時間をナノ秒単位で返します。この関数は、Goランタイムの内部で、イベントのタイミングを測定したり、タイムアウトを計算したりするために使用されます。
runtime·netpoll(false)
runtime·netpoll
は、Goランタイムのネットワークポーラーを呼び出す関数です。引数false
は、この呼び出しがノンブロッキングであることを示します。つまり、ネットワークイベントがすぐに利用可能でない場合でも、この関数はすぐに制御を返します。sysmon
ゴルーチンは、ブロッキングせずにネットワークイベントをチェックし、準備ができたI/O操作を待機しているゴルーチンを再開するためにこれを使用します。
技術的詳細
このコミットが修正するバグは、sysmon
ゴルーチン内のネットワークポーリングロジックにおける論理エラーです。
src/pkg/runtime/proc.c
のsysmon
関数内には、ネットワークポーリングの頻度を制御するための以下のコードがありました。
// poll network if not polled for more than 10ms
lastpoll = runtime·atomicload64(&runtime·sched.lastpoll);
now = runtime·nanotime();
if(lastpoll != 0 && lastpoll + 10*1000*1000 > now) {
runtime·cas64(&runtime·sched.lastpoll, lastpoll, now);
gp = runtime·netpoll(false); // non-blocking
if(gp) {
runtime·injectglist(gp);
}
}
ここで、lastpoll
は前回のネットワークポーリングが実行されたナノ秒単位の時刻、now
は現在のナノ秒単位の時刻です。10*1000*1000
は10ミリ秒をナノ秒に変換した値です。
元の条件式 lastpoll + 10*1000*1000 > now
を見てみましょう。
これは、now - lastpoll < 10*1000*1000
と同等です。
つまり、「現在の時刻と前回のポーリング時刻の差が10ミリ秒未満である場合」に真となります。
しかし、コメントには // poll network if not polled for more than 10ms
と書かれています。これは、「最後にポーリングしてから10ミリ秒以上が経過した場合にネットワークをポーリングする」という意味です。
したがって、コメントとコードのロジックが逆になっていました。ポーリングが必要なのは、now - lastpoll >= 10*1000*1000
の場合、つまり lastpoll + 10*1000*1000 <= now
の場合です。
このコミットは、この比較演算子を >
から <
に変更することで、この論理エラーを修正しました。
修正後の条件式 lastpoll + 10*1000*1000 < now
は、
now - lastpoll > 10*1000*1000
と同等です。
これは、「現在の時刻と前回のポーリング時刻の差が10ミリ秒より大きい場合」に真となります。これにより、コメントの意図通り、「最後にポーリングしてから10ミリ秒以上が経過した場合」にネットワークポーリングが実行されるようになりました。
この修正により、GoランタイムはネットワークI/Oをより適切に処理し、アプリケーションの応答性と安定性が向上します。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/proc.c
ファイルの一箇所のみです。
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -2382,7 +2382,7 @@ sysmon(void)
// poll network if not polled for more than 10ms
lastpoll = runtime·atomicload64(&runtime·sched.lastpoll);
now = runtime·nanotime();
- if(lastpoll != 0 && lastpoll + 10*1000*1000 > now) {
+ if(lastpoll != 0 && lastpoll + 10*1000*1000 < now) {
runtime·cas64(&runtime·sched.lastpoll, lastpoll, now);
gp = runtime·netpoll(false); // non-blocking
if(gp) {
コアとなるコードの解説
変更された行は、sysmon
ゴルーチンがネットワークポーリングを実行するかどうかを決定するif
文の条件式です。
元のコード:
if(lastpoll != 0 && lastpoll + 10*1000*1000 > now) {
修正後のコード:
if(lastpoll != 0 && lastpoll + 10*1000*1000 < now) {
この変更のポイントは、比較演算子が>
(より大きい)から<
(より小さい)に変わったことです。
lastpoll != 0
: これは、lastpoll
が初期値(0)でないことを確認しています。lastpoll
が0の場合、まだ一度もポーリングが実行されていないことを意味するため、この条件はポーリングの初回実行時には関係ありません。lastpoll + 10*1000*1000
: これは、前回のポーリング時刻lastpoll
に10ミリ秒(ナノ秒単位)を加算した値です。これは、次にポーリングを実行すべき「最小の時刻」を示します。now
: これは現在の時刻です。
元のロジック (>
):
lastpoll + 10*1000*1000 > now
これは、「次にポーリングすべき最小時刻が現在の時刻よりも未来である」という意味になります。つまり、「まだ10ミリ秒が経過していない」場合に真となり、ポーリングが実行されていました。これは、ポーリングを抑制したい場合にポーリングを実行してしまうという逆の動作です。
修正後のロジック (<
):
lastpoll + 10*1000*1000 < now
これは、「次にポーリングすべき最小時刻が現在の時刻よりも過去である」という意味になります。つまり、「10ミリ秒がすでに経過している」場合に真となり、ポーリングが実行されます。これにより、コメントの意図通り、「最後にポーリングしてから10ミリ秒以上が経過した場合」にのみネットワークポーリングが実行されるようになりました。
この単純な比較演算子の変更が、GoランタイムのネットワークI/O処理の正確性と効率性を大きく改善しました。
関連リンク
- Go Issue 6610: https://github.com/golang/go/issues/6610
- Go CL 14793043: https://golang.org/cl/14793043
参考にした情報源リンク
- Goのソースコード (
src/pkg/runtime/proc.c
) - GoのIssueトラッカー
- Goのコードレビューシステム (Gerrit)
- Goランタイムのドキュメント (一般的なGoスケジューラと
sysmon
の概念について) - アトミック操作に関する一般的な情報