[インデックス 10381] ファイルの概要
コミット
- コミットハッシュ: dc6726b37f54b0ae3db471de7f1631e6b5cf80e5
- 作成者: Dmitriy Vyukov dvyukov@google.com
- 作成日時: 2011年11月14日 21:59:48 +0300
- タイトル: runtime: fix timers crash
- 説明: Timer callbacks occasionally crash with "sched while holding locks" message.
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/dc6726b37f54b0ae3db471de7f1631e6b5cf80e5
元コミット内容
このコミットは、Go 1.0リリース前の初期段階におけるタイマーの実装に関する重要なバグ修正を行っています。変更内容は以下の通りです:
変更されたファイル:
src/pkg/runtime/time.goc
(8行追加、1行削除)src/pkg/time/sleep_test.go
(16行追加)
修正内容:
- タイマーコールバック実行時にロックを保持したまま実行されることによる「sched while holding locks」エラーの修正
- タイマーコールバック実行前にロックを解放し、実行後に再度ロックを取得するように変更
- ストレステストの追加
変更の背景
Go 1.0のリリース前(2011年)において、タイマーのコールバック実行時に発生する重要なデッドロックの問題が発見されました。この問題は以下の状況で発生していました:
-
タイマーコールバックの実行中にロックを保持: タイマーコールバック関数が実行される際、runtimeのタイマーロック(
timers
)を保持したまま実行されていました。 -
スケジューラのデッドロック: コールバック関数内でgoroutineのスケジューリングが必要な処理(メモリ割り当て、チャンネル操作など)が発生すると、スケジューラがロックを取得しようとしてデッドロックが発生しました。
-
「sched while holding locks」エラー: このデッドロック状態が検出されると、Goランタイムは「sched while holding locks」エラーメッセージを出力してクラッシュしていました。
前提知識の解説
Goランタイムのタイマー実装
Go 1.0初期のタイマー実装では、以下のような構造でタイマーが管理されていました:
- タイマーキュー: 全てのタイマーはグローバルなヒープ(priority queue)で管理されています
- タイマープロセス: 専用のgoroutine(
timerproc
)が常駐し、タイマーの期限をチェックしています - タイマーロック: 複数のgoroutineからのタイマー操作を同期するためのグローバルロック(
timers
)
「sched while holding locks」エラーの意味
このエラーは、Goランタイムの内部的な安全性チェックによって発生します:
- ロック保持中のスケジューリング禁止: ランタイムの内部ロックを保持している間は、goroutineのスケジューリングを行ってはいけません
- デッドロック回避: この制約により、異なるgoroutineが同じロックを取得しようとして無限に待機するデッドロックを防いでいます
- エラー検出: スケジューラがロック保持中にgoroutineをスケジュールしようとすると、この安全性チェックが発動します
タイマーコールバックの実行タイミング
タイマーコールバックは、以下の流れで実行されます:
timerproc
がタイマーキューを監視- 期限が来たタイマーを発見
- タイマーをキューから削除
- コールバック関数を実行
- 次のタイマーをチェック
技術的詳細
修正前のコード(問題のあるコード)
// 問題のあった実装
runtime·lock(&timers);
// ... タイマーの処理 ...
t->f(now, t->arg); // ロックを保持したままコールバック実行
修正後のコード(修正版)
// 修正後の実装
runtime·lock(&timers);
// ... タイマーの処理 ...
f = t->f; // コールバック関数を一時変数に保存
arg = t->arg; // 引数を一時変数に保存
runtime·unlock(&timers); // ロックを解放
f(now, arg); // ロックを保持せずにコールバック実行
runtime·lock(&timers); // ロックを再取得
修正の核心的な考え方
- ロックの最小化: コールバック実行時にはロックを保持しない
- データの一時保存: コールバック関数と引数を一時変数に保存してから実行
- 安全なロック管理: ロックの解放と再取得を適切なタイミングで行う
コアとなるコードの変更箇所
src/pkg/runtime/time.goc の変更
変更前:
timerproc(void)
{
int64 delta, now;
Timer *t;
for(;;) {
runtime·lock(&timers);
// ... タイマー処理 ...
t->f(now, t->arg); // 問題: ロック保持中にコールバック実行
// ...
}
}
変更後:
timerproc(void)
{
int64 delta, now;
Timer *t;
void (*f)(int64, Eface); // 追加: コールバック関数用の変数
Eface arg; // 追加: 引数用の変数
for(;;) {
runtime·lock(&timers);
// ... タイマー処理 ...
f = t->f; // 追加: コールバック関数を保存
arg = t->arg; // 追加: 引数を保存
runtime·unlock(&timers); // 追加: ロック解放
f(now, arg); // 修正: ロックを保持せずに実行
runtime·lock(&timers); // 追加: ロック再取得
// ...
}
}
src/pkg/time/sleep_test.go の変更
新たに追加されたTestAfterStress
関数:
func TestAfterStress(t *testing.T) {
stop := uint32(0)
go func() {
for atomic.LoadUint32(&stop) == 0 {
runtime.GC() // ガベージコレクションを並行実行
}
}()
c := Tick(1)
for i := 0; i < 100; i++ {
<-c // タイマーからの値を受信
}
atomic.StoreUint32(&stop, 1)
}
コアとなるコードの解説
1. タイマープロセスの構造
timerproc
は、Goランタイムの心臓部とも言える関数です:
- 無限ループ: タイマーキューを継続的に監視
- ロック管理:
runtime·lock(&timers)
でグローバルなタイマーロックを取得 - タイマー処理: 期限切れのタイマーをキューから削除し、コールバックを実行
2. 修正のポイント
問題の核心:
- コールバック関数
t->f(now, t->arg)
が、ロックを保持したまま実行されていた - コールバック内でスケジューリングが必要な処理が発生すると、デッドロックが発生
修正の内容:
- 一時変数の導入:
f
とarg
でコールバック関数と引数を保存 - ロックの解放: コールバック実行前に
runtime·unlock(&timers)
でロックを解放 - 安全な実行: ロックを保持せずにコールバック
f(now, arg)
を実行 - ロックの再取得: コールバック実行後に
runtime·lock(&timers)
でロックを再取得
3. ストレステストの意義
TestAfterStress
関数は、この修正が有効であることを確認するためのテストです:
- 並行ガベージコレクション:
runtime.GC()
を並行実行してメモリ管理の負荷を増加 - 高頻度タイマー:
Tick(1)
で1ナノ秒間隔のタイマーを作成 - 長時間実行: 100回のタイマー受信で持続的な負荷をかける
このテストにより、修正前であれば発生していた「sched while holding locks」エラーが発生しないことを確認できます。
4. 修正による影響
正の影響:
- デッドロックの解消
- タイマーコールバックの安全な実行
- システムの安定性向上
考慮すべき点:
- コールバック実行中にタイマーロックが解放されるため、他のgoroutineがタイマーを操作可能
- ロックの取得/解放によるわずかなオーバーヘッド