[インデックス 16693] ファイルの概要
このコミットは、Go言語のtime
パッケージにおけるタイマーの削除処理に関する重要なバグ修正です。具体的には、タイマーを削除する際にnil
ポインタのデリファレンスによってパニックが発生した場合に、タイマーのミューテックスがロックされたままになることを防ぎ、それによって発生しうるデッドロックの問題を解決します。
コミット
commit 0286b4738e33c5a043d454b23af88fb95127bf13
Author: Jeff R. Allen <jra@nella.org>
Date: Mon Jul 1 21:42:29 2013 -0400
time: prevent a panic from leaving the timer mutex held
When deleting a timer, a panic due to nil deref
would leave a lock held, possibly leading to a deadlock
in a defer. Instead return false on a nil timer.
Fixes #5745.
R=golang-dev, daniel.morsing, dvyukov, rsc, iant
CC=golang-dev
https://golang.org/cl/10373047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0286b4738e33c5a043d454b23af88fb95127bf13
元コミット内容
このコミットは、Goのtime
パッケージにおいて、タイマーを削除する際にnil
ポインタのデリファレンス(参照外し)によってパニックが発生した場合に、タイマー管理用のミューテックスがロックされたままになる問題を修正します。この状態は、defer
ステートメント内でタイマーの停止処理(Stop()
)が呼ばれた際にデッドロックを引き起こす可能性がありました。修正は、ロックを取得する前にnil
デリファレンスによるパニックを意図的に発生させることで、ミューテックスがロックされる前に処理を中断させるというアプローチを取っています。
変更の背景
Goのランタイムには、time
パッケージが提供するタイマーやTickerを管理するための内部的なメカニズムが存在します。これらのメカニズムは、複数のゴルーチンからの同時アクセスを安全に処理するために、ミューテックス(相互排他ロック)を使用して共有リソースを保護しています。
このコミットが修正する問題は、runtime·deltimer
関数(タイマー削除の内部関数)がミューテックスを取得した後に、nil
ポインタのデリファレンスによってパニックが発生した場合に顕在化しました。具体的には、*Timer
型の変数がnil
であるにもかかわらず、そのメソッド(例: Stop()
)が呼び出された場合、Goランタイムはnil
ポインタデリファレンスとしてパニックを発生させます。
問題のシナリオは以下の通りです。
- あるゴルーチンがタイマーを停止しようと
timer.Stop()
を呼び出す。 - 内部的に
runtime·deltimer
関数が呼ばれる。 runtime·deltimer
関数が、タイマー管理用のミューテックス(timers
)をロックする。- このロック取得後、かつロック解放前に、
timer
がnil
であるためにnil
デリファレンスによるパニックが発生する。 - パニックが発生すると、通常のコードフローは中断され、
defer
ステートメントが実行される。 - もし
defer
ステートメント内で別のタイマーのStop()
が呼ばれた場合、そのStop()
は既にロックされたtimers
ミューテックスを再度取得しようとする。 - 結果として、ミューテックスが解放されないまま別のロック要求が発生し、デッドロックが発生する。
このデッドロックは、特にテストコードやエラーハンドリングでdefer
を使ってリソースをクリーンアップするような場合に、プログラムがハングアップする原因となっていました。コミットメッセージにあるFixes #5745
は、この問題がGoのIssueトラッカーで報告されていたことを示しています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とランタイムの動作に関する知識が必要です。
-
Goの
time
パッケージとタイマー/Ticker:time.Timer
とtime.Ticker
は、指定された時間後に一度だけイベントを発生させる(Timer)か、定期的にイベントを発生させる(Ticker)ためのGoの標準ライブラリ機能です。- これらは内部的にGoランタイムのスケジューラと連携し、効率的に時間ベースのイベントを管理します。
Stop()
メソッドは、タイマーやTickerの動作を停止し、関連するリソースを解放するために使用されます。
-
ミューテックス (Mutex):
sync.Mutex
は、Goにおける相互排他ロックのプリミティブです。共有リソースへの同時アクセスを制御し、データ競合を防ぐために使用されます。Lock()
メソッドでロックを取得し、Unlock()
メソッドでロックを解放します。- ミューテックスがロックされている間に別のゴルーチンが同じミューテックスをロックしようとすると、そのゴルーチンはロックが解放されるまでブロックされます。
- Goランタイムの内部でも、タイマー管理のような共有データ構造を保護するためにミューテックスが使用されます。
-
パニック (Panic) とリカバリー (Recover):
- Goにおけるパニックは、プログラムの異常終了を示すメカニズムです。通常、プログラミングエラー(例:
nil
ポインタデリファレンス、配列の範囲外アクセス)や回復不能なエラーが発生した場合に引き起こされます。 - パニックが発生すると、現在のゴルーチンの実行は中断され、そのゴルーチン内で遅延実行される
defer
関数が順に実行されます。 recover()
組み込み関数は、defer
関数内でのみ呼び出すことができ、パニックを捕捉してプログラムの実行を再開させることができます。しかし、このコミットの文脈では、パニックを捕捉するのではなく、ミューテックスがロックされる前にパニックを発生させることが目的です。
- Goにおけるパニックは、プログラムの異常終了を示すメカニズムです。通常、プログラミングエラー(例:
-
defer
ステートメント:defer
ステートメントは、その関数がリターンする直前、またはパニックが発生して関数が終了する直前に実行される関数呼び出しをスケジュールします。- リソースの解放(ファイルクローズ、ロック解除など)やエラーハンドリングによく使用されます。
- この問題では、
defer
内でStop()
が呼ばれることで、ロックされたミューテックスを再度取得しようとしてデッドロックが発生するという点が重要です。
-
nil
ポインタデリファレンス:- Goでは、ポインタが
nil
であるにもかかわらず、そのポインタが指す値にアクセスしようとしたり、nil
ポインタのメソッドを呼び出したりすると、ランタイムパニックが発生します。 - このコミットの根本原因は、
nil
の*Timer
に対してStop()
が呼ばれることでした。
- Goでは、ポインタが
技術的詳細
このコミットの技術的な解決策は、非常に巧妙かつGoランタイムの特性を理解したものです。問題は「ミューテックスをロックした後にnil
デリファレンスによるパニックが発生し、ミューテックスが解放されない」ことでした。この問題を解決するために、コミットは以下の変更をruntime·deltimer
関数に導入しました。
変更前:
// runtime·deltimer(Timer *t)
// {
// int32 i;
// runtime·lock(&timers); // ここでロックを取得
// // ... t を使用する処理 ...
// }
変更後 (src/pkg/runtime/time.goc
):
void
runtime·deltimer(Timer *t)
{
int32 i;
// Dereference t so that any panic happens before the lock is held.
// Discard result, because t might be moving in the heap.
i = t->i; // ここで t のデリファレンスを試みる
USED(i);
runtime·lock(&timers); // ロックは t のデリファレンス後に行われる
// ...
}
この変更の核心は、runtime·lock(&timers)
(タイマー管理用のミューテックスをロックする処理)の前に、引数として渡されたTimer *t
のメンバt->i
にアクセスする行i = t->i;
を追加したことです。
- もし
t
が有効なTimer
ポインタであれば、t->i
へのアクセスは問題なく成功し、i
に値が代入されます。USED(i)
は、コンパイラが未使用変数として警告を出さないようにするためのGoランタイムの慣用句です。 - もし
t
がnil
ポインタであった場合、i = t->i;
の行でnil
ポインタデリファレンスが発生し、ミューテックスがロックされる前にパニックが引き起こされます。
これにより、パニックが発生してもtimers
ミューテックスはロックされていない状態が保証されます。結果として、defer
ステートメント内でStop()
が呼ばれても、デッドロックが発生する可能性がなくなります。
このアプローチは、Goのパニックメカニズムを逆手に取ったものであり、エラーハンドリングの一般的なパターンとは異なりますが、ランタイムの低レベルなコードにおいてはこのような最適化や安全策が講じられることがあります。
また、src/pkg/time/sleep_test.go
には、この問題が修正されたことを検証するための新しいテストケースTestIssue5745
が追加されました。このテストは、意図的にnil
の*Timer
に対してStop()
を呼び出し、パニックが発生すること、そしてそのパニックがdefer
内のticker.Stop()
をデッドロックさせないことを確認します。
コアとなるコードの変更箇所
src/pkg/runtime/time.goc
--- a/src/pkg/runtime/time.goc
+++ b/src/pkg/runtime/time.goc
@@ -131,6 +131,11 @@ runtime·deltimer(Timer *t)
{\n \tint32 i;\n \n+\t// Dereference t so that any panic happens before the lock is held.\n+\t// Discard result, because t might be moving in the heap.\n+\ti = t->i;\n+\tUSED(i);\n+\n \truntime·lock(&timers);\
\n \t// t may not be registered anymore and may have
src/pkg/time/sleep_test.go
--- a/src/pkg/time/sleep_test.go
+++ b/src/pkg/time/sleep_test.go
@@ -314,3 +314,23 @@ func TestOverflowSleep(t *testing.T) {\n \t\tt.Fatalf(\"negative timeout didn\'t fire\")\n \t}\n }\n+\n+// Test that a panic while deleting a timer does not leave\n+// the timers mutex held, deadlocking a ticker.Stop in a defer.\n+func TestIssue5745(t *testing.T) {\n+\tticker := NewTicker(Hour)\n+\tdefer func() {\n+\t\t// would deadlock here before the fix due to\n+\t\t// lock taken before the segfault.\n+\t\tticker.Stop()\n+\n+\t\tif r := recover(); r == nil {\n+\t\t\tt.Error(\"Expected panic, but none happened.\")\n+\t\t}\n+\t}()\n+\n+\t// cause a panic due to a segfault\n+\tvar timer *Timer\n+\ttimer.Stop()\n+\tt.Error(\"Should be unreachable.\")\n+}\
コアとなるコードの解説
src/pkg/runtime/time.goc
の変更
runtime·deltimer
関数は、Goランタイム内部でタイマーを削除する際に呼び出されるC言語で書かれた関数です(Goのソースコードには.goc
拡張子のファイルがあり、これはC言語とGoのハイブリッドのような形式で、Goランタイムの低レベルな部分を実装するために使われます)。
追加された5行のコードは以下の通りです。
// Dereference t so that any panic happens before the lock is held.
// Discard result, because t might be moving in the heap.
i = t->i;
USED(i);
i = t->i;
: ここが最も重要な変更点です。t
はTimer *
型のポインタであり、i
はそのTimer
構造体内のメンバ(おそらくタイマーのインデックスやIDのようなもの)です。この行は、runtime·lock(&timers)
が呼び出される前に、意図的にt
をデリファレンスしようとします。- もし
t
がnil
であれば、この行でnil
ポインタデリファレンスによるパニックが発生します。 - パニックが発生すると、それ以降のコード(
runtime·lock(&timers)
を含む)は実行されません。 - これにより、
timers
ミューテックスがロックされる前にパニックが処理されることが保証され、ミューテックスがロックされたままになることを防ぎます。
- もし
USED(i);
: これはGoランタイムの内部マクロで、変数i
が使用されていることをコンパイラに伝えるためのものです。i
の値自体はこの修正のロジックには直接関係なく、単にt
がデリファレンス可能かどうかをチェックするためにアクセスされています。
この変更により、nil
タイマーに対するStop()
呼び出しは、ミューテックスをロックする前にパニックを引き起こすようになり、デッドロックの可能性が排除されました。
src/pkg/time/sleep_test.go
の変更
TestIssue5745
という新しいテスト関数が追加されました。このテストは、修正が正しく機能することを確認するためのものです。
func TestIssue5745(t *testing.T) {
ticker := NewTicker(Hour)
defer func() {
// would deadlock here before the fix due to
// lock taken before the segfault.
ticker.Stop()
if r := recover(); r == nil {
t.Error("Expected panic, but none happened.")
}
}()
// cause a panic due to a segfault
var timer *Timer
timer.Stop()
t.Error("Should be unreachable.")
}
ticker := NewTicker(Hour)
: テストのためにTicker
を作成します。このTicker
は、defer
内でStop()
が呼ばれることで、デッドロックが発生するかどうかを検証するためのものです。defer func() { ... }()
: 無名関数をdefer
で登録しています。この関数は、TestIssue5745
が終了する際に実行されます。ticker.Stop()
: ここがデッドロックの発生源となる可能性があった場所です。修正前は、もしメインのコードでパニックが発生し、ミューテックスがロックされたままだった場合、このStop()
呼び出しがデッドロックを引き起こしました。修正後は、デッドロックが発生しないことを期待します。if r := recover(); r == nil { t.Error("Expected panic, but none happened.") }
:recover()
を使ってパニックを捕捉し、実際にパニックが発生したことを確認しています。このテストの目的は、nil
デリファレンスによるパニックが発生すること、そしてそのパニックがデッドロックを引き起こさないことを検証することです。
var timer *Timer
:*Timer
型の変数を宣言しますが、初期化しないため、その値はnil
になります。timer.Stop()
: この行が意図的にnil
ポインタデリファレンスによるパニックを引き起こします。修正前は、この呼び出しがruntime·deltimer
内でミューテックスをロックした後にパニックを引き起こし、デッドロックにつながっていました。修正後は、ミューテックスがロックされる前にパニックが発生します。t.Error("Should be unreachable.")
:timer.Stop()
でパニックが発生するため、この行は実行されません。もし実行された場合、テストは失敗します。
このテストは、問題の再現と修正の検証を同時に行う、非常に効果的なテストケースです。
関連リンク
- Gerrit Change-ID:
https://golang.org/cl/10373047
- Goプロジェクトは、GitHubに移行する前はGerritというコードレビューシステムを使用していました。このリンクは、当時のGerrit上の変更セット(Change-ID)を指しています。
参考にした情報源リンク
- Go言語の公式ドキュメント(
time
パッケージ、sync
パッケージ、パニックとリカバリーに関するセクション) - Goランタイムのソースコード(特に
src/pkg/runtime/time.goc
) - GoのIssueトラッカー(ただし、
#5745
という直接のIssueは見つかりませんでした。これは古いIssueであるか、内部的なトラッキング番号である可能性があります。)