Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 13195] ファイルの概要

このコミットは、Go言語のtimeパッケージにおけるSleep(0)呼び出しが特定の条件下でデッドロックを引き起こすバグを修正するものです。具体的には、Sleep(0)がゴルーチンの状態を不適切に設定し、その後にガベージコレクション(GC)が実行されると、ゴルーチンがスケジューラによって再開されなくなる問題に対処しています。

コミット

commit a0efca84e61f6d98587d8b49d69c78bdc1acc6b4
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue May 29 22:30:56 2012 +0400

    time: fix deadlock in Sleep(0)
    See time/sleep_test.go for repro.
    
    R=golang-dev, r, rsc
    CC=golang-dev, patrick.allen.higgins
    https://golang.org/cl/6250072
---
 src/pkg/runtime/time.goc   |  5 ++++-\n src/pkg/time/sleep_test.go | 22 ++++++++++++++++++++++\n 2 files changed, 26 insertions(+), 1 deletion(-)

diff --git a/src/pkg/runtime/time.goc b/src/pkg/runtime/time.goc
index a6b8352470..b18902f00f 100644
--- a/src/pkg/runtime/time.goc
+++ b/src/pkg/runtime/time.goc
@@ -61,8 +61,11 @@ runtime·tsleep(int64 ns)\n {\n 	Timer t;\n \n-\tif(ns <= 0)\n+\tif(ns <= 0) {\n+\t\tg->status = Grunning;\n+\t\tg->waitreason = nil;\n \t\treturn;\n+\t}\n \n \tt.when = runtime·nanotime() + ns;\n \tt.period = 0;\ndiff --git a/src/pkg/time/sleep_test.go b/src/pkg/time/sleep_test.go
index 526d58d75e..e05773df6e 100644
--- a/src/pkg/time/sleep_test.go
+++ b/src/pkg/time/sleep_test.go
@@ -223,3 +223,25 @@ func TestTimerStopStress(t *testing.T) {\n \t}\n \tSleep(3 * Second)\n }\n+\n+func TestSleepZeroDeadlock(t *Service.T) {\n+\t// Sleep(0) used to hang, the sequence of events was as follows.\n+\t// Sleep(0) sets G's status to Gwaiting, but then immediately returns leaving the status.\n+\t// Then the goroutine calls e.g. new and falls down into the scheduler due to pending GC.\n+\t// After the GC nobody wakes up the goroutine from Gwaiting status.\n+\tdefer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4))\n+\tc := make(chan bool)\n+\tgo func() {\n+\t\tfor i := 0; i < 100; i++ {\n+\t\t\truntime.GC()\n+\t\t}\n+\t\tc <- true\n+\t}()\n+\tfor i := 0; i < 100; i++ {\n+\t\tSleep(0)\n+\t\ttmp := make(chan bool, 1)\n+\t\ttmp <- true\n+\t\t<-tmp\n+\t}\n+\t<-c\n+}\n```

## GitHub上でのコミットページへのリンク

[https://github.com/golang/go/commit/a0efca84e61f6d98587d8b49d69c78bdc1acc6b4](https://github.com/golang/go/commit/a0efca84e61f6d98587d8b49d69c78bdc1acc6b4)

## 元コミット内容

time: fix deadlock in Sleep(0) See time/sleep_test.go for repro.

R=golang-dev, r, rsc CC=golang-dev, patrick.allen.higgins https://golang.org/cl/6250072


## 変更の背景

この変更は、Go言語の`time.Sleep(0)`関数が特定の状況下でデッドロックを引き起こすという深刻なバグを修正するために行われました。`Sleep(0)`は、現在のゴルーチンを一時的に中断し、他のゴルーチンに実行を譲る(yieldする)ために使用されることがあります。しかし、以前の実装では、`Sleep(0)`がゴルーチンの内部状態を`Gwaiting`(待機中)に設定したまま即座にリターンしてしまう問題がありました。

この不適切な状態設定が問題となるのは、その後にガベージコレクション(GC)が実行される場合です。GCは、実行中のゴルーチンを一時停止させ、ヒープをスキャンして不要なメモリを解放します。GCが完了した後、停止していたゴルーチンは再開されますが、`Gwaiting`状態のゴルーチンはスケジューラによって「待機中」と見なされ、再開の対象から外れてしまうことがありました。特に、`Sleep(0)`の直後にメモリ割り当て(例: `new`や`make`)が発生し、それがGCをトリガーするような状況で、このデッドロックが発生しやすかったのです。

この問題は、`time/sleep_test.go`に追加された`TestSleepZeroDeadlock`テストケースによって再現されました。このテストは、`Sleep(0)`と`runtime.GC()`の呼び出しを繰り返すことで、デッドロックの発生をシミュレートしています。

## 前提知識の解説

このコミットの理解には、以下のGo言語のランタイムと並行処理に関する知識が不可欠です。

1.  **ゴルーチン (Goroutine)**:
    Go言語における軽量な実行スレッドです。数千、数万のゴルーチンを同時に実行してもオーバーヘッドが少ないのが特徴です。Goランタイムによって管理され、OSのスレッドに多重化されて実行されます。

2.  **ゴルーチンの状態 (Goroutine States)**:
    Goランタイムは、ゴルーチンの内部状態を管理しています。主要な状態には以下のようなものがあります。
    *   `Grunning`: ゴルーチンが現在実行中である状態。
    *   `Gwaiting`: ゴルーチンが何らかのイベント(タイマー、チャネル操作、システムコールなど)を待機している状態。この状態のゴルーチンは、イベントが完了するまでスケジューラによって実行されません。
    *   `Grunnable`: ゴルーチンが実行可能であり、スケジューラによって実行されるのを待っている状態。
    *   `Gdead`: ゴルーチンが終了した状態。

3.  **Goスケジューラ (Go Scheduler)**:
    Goランタイムの重要なコンポーネントで、ゴルーチンをOSスレッドにマッピングし、実行を管理します。スケジューラは、実行可能なゴルーチンを効率的にOSスレッドに割り当て、CPUコアを最大限に活用するように動作します。ゴルーチンがブロックされる(例: `time.Sleep`、チャネル操作、システムコール)と、スケジューラはそのゴルーチンを一時停止し、別の実行可能なゴルーチンにCPUを割り当てます。

4.  **`time.Sleep(duration)`**:
    指定された期間、現在のゴルーチンの実行を一時停止する関数です。`duration`が0の場合、`Sleep(0)`は現在のゴルーチンを一時的に中断し、他のゴルーチンに実行を譲る(yield)ことを意図しています。これは、CPUを占有しすぎないようにしたり、他のゴルーチンに機会を与えたりするために使われます。

5.  **ガベージコレクション (Garbage Collection, GC)**:
    Goランタイムに組み込まれている自動メモリ管理機能です。プログラムが動的に割り当てたメモリのうち、もはや到達不可能(参照されていない)になったメモリ領域を自動的に解放します。GoのGCは並行(concurrent)かつ低遅延(low-latency)で動作するように設計されており、プログラムの実行と同時にGC処理の一部を進めることができます。しかし、GCの特定のフェーズ(例: マークフェーズの開始)では、すべてのゴルーチンを一時的に停止させる必要があります(Stop-The-Worldフェーズ)。

## 技術的詳細

デッドロックの根本原因は、`runtime·tsleep`関数(`time.Sleep`の実装の一部)が`ns <= 0`(つまり`Sleep(0)`)の場合に、ゴルーチンの状態を適切にリセットせずに即座にリターンしていたことにありました。

以前の`runtime·tsleep`の実装では、`ns > 0`の場合、ゴルーチンはタイマーイベントを待機するために`Gwaiting`状態に遷移し、タイマーが完了するとスケジューラによって`Grunnable`状態に戻され、再開されます。しかし、`ns <= 0`の場合、ゴルーチンは実際には何も待機しないため、すぐにリターンすることが期待されていました。

問題は、`Sleep(0)`が呼び出される前に、何らかの理由でゴルーチンが`Gwaiting`状態になっていた場合、または`Sleep`関数の内部ロジックが`Gwaiting`状態に遷移させてしまった場合に発生しました。`Sleep(0)`は、タイマーを設定するロジックをスキップしてすぐにリターンするため、ゴルーチンが`Gwaiting`状態のままであるにもかかわらず、その状態を`Grunning`に戻す処理が行われませんでした。

この状態で、もしそのゴルーチンがメモリ割り当てを行い、それがGCをトリガーした場合、GCは`Gwaiting`状態のゴルーチンを「待機中」と見なし、GC完了後に再開すべきゴルーチンリストから除外してしまう可能性がありました。結果として、ゴルーチンは`Gwaiting`状態のまま永久に停止し、デッドロックが発生しました。

`TestSleepZeroDeadlock`テストケースは、このシナリオを再現するために設計されています。
1.  `runtime.GOMAXPROCS(4)`を設定し、複数のOSスレッドでゴルーチンが実行される環境をシミュレートします。
2.  別のゴルーチンで`runtime.GC()`を繰り返し呼び出し、GCを頻繁にトリガーします。
3.  メインのゴルーチンで`Sleep(0)`を繰り返し呼び出し、その直後に`make(chan bool, 1)`のようなメモリ割り当てを行います。これにより、`Sleep(0)`による状態の不整合と、GCによるゴルーチン停止・再開の相互作用が引き起こされ、デッドロックが再現されます。

## コアとなるコードの変更箇所

`src/pkg/runtime/time.goc`ファイルの`runtime·tsleep`関数における変更点です。

```diff
--- a/src/pkg/runtime/time.goc
+++ b/src/pkg/runtime/time.goc
@@ -61,8 +61,11 @@ runtime·tsleep(int64 ns)\n {\n 	Timer t;\n \n-\tif(ns <= 0)\n+\tif(ns <= 0) {\n+\t\tg->status = Grunning;\n+\t\tg->waitreason = nil;\n \t\treturn;\n+\t}\n \n \tt.when = runtime·nanotime() + ns;\

コアとなるコードの解説

変更は、runtime·tsleep関数内のif(ns <= 0)ブロックに2行のコードを追加したことです。

g->status = Grunning;
g->waitreason = nil;
  • g->status = Grunning;: これは、現在のゴルーチン(g)の内部状態を明示的にGrunningに設定するものです。Sleep(0)が呼び出された場合、ゴルーチンは実際には待機状態に入るべきではありません。この修正により、Sleep(0)が即座にリターンする際に、ゴルーチンが確実に実行可能な状態に戻されることが保証されます。これにより、GCがゴルーチンを誤ってGwaiting状態のまま放置するのを防ぎます。

  • g->waitreason = nil;: waitreasonは、ゴルーチンがGwaiting状態にある場合に、なぜ待機しているのかを示す文字列です。Sleep(0)の場合、ゴルーチンは特定の理由で待機しているわけではないため、このフィールドをnil(またはGoの""に相当)にクリアします。これにより、ゴルーチンの状態がより正確に反映され、デバッグやプロファイリングの際にも誤解を招く情報が提供されるのを防ぎます。

これらの変更により、Sleep(0)が呼び出されたゴルーチンは、たとえ一時的にGwaiting状態に遷移していたとしても、関数からリターンする前に必ずGrunning状態に戻され、スケジューラによって適切に扱われるようになります。これにより、GCとの相互作用によるデッドロックが解消されました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (runtime, timeパッケージ)
  • Go言語のスケジューラに関する記事 (例: "Go's work-stealing scheduler")
  • Go言語のガベージコレクションに関する記事 (例: "Go's concurrent garbage collector")
  • Go言語のソースコード (src/pkg/runtime/proc.c, src/pkg/runtime/runtime.h など)
  • Go言語のIssueトラッカー (関連するバグ報告や議論)