[インデックス 12775] ファイルの概要
このコミットは、以前のコミット CL 5844051 / 5d0322034aa8
を元に戻す(undo)ものです。元のコミットはGoランタイムにおけるデッドロック検出の改善を試みましたが、この変更がGOMAXPROCS=2
以上の環境でクロージャテストを破壊するという問題を引き起こしたため、その修正を元に戻すことになりました。具体的には、ランタイムのスケジューラとメモリヒープの管理に関連するコードから、デッドロック検出ロジックの一部と、runtime·gosched()
の呼び出しが削除されています。また、元のコミットで追加されたデッドロックテストファイル test/fixedbugs/bug429.go
も削除されています。
コミット
undo CL 5844051 / 5d0322034aa8
Breaks closure test when GOMAXPROCS=2 or more.
««« original CL description
runtime: restore deadlock detection in the simplest case.
Fixes #3342.
R=iant, r, dave, rsc
CC=golang-dev, remy
https://golang.org/cl/5844051
»»»
R=rsc
CC=golang-dev
https://golang.org/cl/5924045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4c2614c57c5e93431aef95490dd2de956ceb9967
元コミット内容
このコミットが元に戻している CL 5844051
の元の説明は以下の通りです。
runtime: restore deadlock detection in the simplest case.
Fixes #3342.
これは、Goランタイムにおいて最も単純なケースでのデッドロック検出機能を復元しようとするものであり、GoのIssue #3342を修正することを目的としていました。
変更の背景
元のコミット CL 5844051
は、Goランタイムのデッドロック検出を改善するために導入されました。しかし、この変更が予期せぬ副作用を引き起こしました。具体的には、GOMAXPROCS
環境変数が2以上(つまり、複数のOSスレッドがGoのランタイムスケジューラによって利用されるマルチコア環境)に設定されている場合に、Goのクロージャ(closure)に関連するテストが失敗するようになりました。
Goのランタイムは、複数のゴルーチン(goroutine)を効率的にスケジューリングし、並行処理を実現します。デッドロックは、複数のゴルーチンがお互いにリソースを待機し、どのゴルーチンも処理を進められなくなる状態を指します。ランタイムがデッドロックを検出することは、プログラムがハングアップするのを防ぎ、開発者に問題の存在を知らせる上で重要です。
しかし、デッドロック検出ロジックは非常に複雑であり、特にマルチプロセッサ環境では、ゴルーチンの状態遷移やスケジューリングのタイミングが複雑に絡み合うため、誤検知や、本来デッドロックではない状況をデッドロックと判断してしまう「偽陽性」が発生する可能性があります。このコミットは、元のデッドロック検出の変更が、特定のマルチコア環境でのテスト失敗という形で偽陽性または不適切な挙動を引き起こしたため、その変更を元に戻すという判断がなされたことを示しています。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。
- ゴルーチン (Goroutine): Goにおける軽量な並行実行単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムがゴルーチンのスケジューリング、スタックの管理、通信(チャネル)などを担当します。
- Goランタイム (Go Runtime): Goプログラムの実行を管理するシステムです。ゴルーチンのスケジューリング、メモリ管理(ガベージコレクション)、チャネル通信、システムコールとの連携など、Goプログラムの低レベルな動作のほとんどを制御します。
- スケジューラ (Scheduler): Goランタイムの一部で、ゴルーチンをOSスレッド(M: Machine)に割り当て、実行を管理します。GoのスケジューラはM:Nスケジューリングモデルを採用しており、N個のゴルーチンをM個のOSスレッド上で実行します。
GOMAXPROCS
: Goプログラムが同時に実行できるOSスレッドの最大数を設定する環境変数です。デフォルトではCPUのコア数に設定されます。GOMAXPROCS=1
の場合、Goプログラムは単一のOSスレッド上で実行され、真の並行処理は行われません(並行処理はゴルーチンの協調的なマルチタスクによって実現されます)。GOMAXPROCS=2
以上の場合、複数のOSスレッドが利用され、真の並行処理が可能になります。- デッドロック (Deadlock): 複数のゴルーチンが互いに相手が保持しているリソースの解放を待ち、結果としてどのゴルーチンも処理を進められなくなる状態です。Goランタイムは、すべてのゴルーチンがスリープ状態になり、かつ今後も実行可能になる見込みがない場合にデッドロックを検出してパニック(
all goroutines are asleep - deadlock!
)を発生させます。 runtime·gosched()
: Goランタイムの内部関数で、現在のゴルーチンを一時停止し、他の実行可能なゴルーチンにCPUを譲る(yield)ためのものです。これにより、協調的なマルチタスクが実現され、一つのゴルーチンがCPUを占有し続けることを防ぎます。runtime·throw()
: Goランタイムの内部関数で、致命的なエラーが発生した場合にパニックを引き起こし、プログラムを終了させます。デッドロック検出時にも使用されます。scvg
(Scavenger Goroutine): Goランタイムのガベージコレクタの一部として動作する特別なゴルーチンです。メモリの解放やヒープの整理といったバックグラウンドタスクを担当します。runtime·sched.grunning
: スケジューラが現在実行中または実行可能な状態にあるゴルーチンの数を追跡するランタイム内部の変数です。runtime·sched.gwait
: スケジューラが現在何らかのイベント(I/O完了、チャネルからの受信など)を待機しているゴルーチンの数を追跡するランタイム内部の変数です。
技術的詳細
このコミットは、Goランタイムのデッドロック検出ロジックと、ガベージコレクタのスカベンジャーゴルーチンの挙動に影響を与えます。
-
src/pkg/runtime/mheap.c
の変更:runtime·MHeap_Scavenger
関数からruntime·gosched();
の呼び出しが削除されました。- 元のコミットでは、スカベンジャーゴルーチンがヒープの整理を行うループ内で定期的に
runtime·gosched()
を呼び出すことで、他のゴルーチンにCPUを譲り、デッドロック状態の検出を助ける意図があったと考えられます。しかし、これがマルチプロセッサ環境でのクロージャテストに悪影響を与えたため、元に戻されました。この変更により、スカベンジャーゴルーチンは明示的にCPUを譲ることなく、自身のタスクを継続して実行するようになります。
-
src/pkg/runtime/proc.c
の変更:checkdeadlock
という静的関数が完全に削除されました。この関数は、スケジューラの状態(実行中のゴルーチン数、待機中のゴルーチン数、スカベンジャーゴルーチンの状態)をチェックしてデッドロックを検出する役割を担っていました。checkdeadlock
関数が呼び出されていた箇所(top:
ラベルの直後と、runtime·sched.grunning++
の直後)から、その呼び出しが削除されました。- 代わりに、
top:
ラベルの直後で、checkdeadlock
関数内のデッドロック検出ロジックがインライン化されました。ただし、インライン化された条件式には重要な変更があります。元のcheckdeadlock
関数内の条件式(scvg->status == Grunnable || scvg->status == Grunning || scvg->status == Gsyscall)
から、scvg->status == Grunnable
の部分が削除されています。Grunnable
はゴルーチンが実行可能状態であることを示します。スカベンジャーゴルーチンが実行可能状態であっても、それがデッドロックの条件に含められると、特定の状況下で誤ってデッドロックと判断される可能性があったのかもしれません。この変更により、スカベンジャーゴルーチンが実行中 (Grunning
) またはシステムコール中 (Gsyscall
) の場合にのみ、デッドロック検出の条件に考慮されるようになります。これは、デッドロック検出の厳密さを緩和し、偽陽性を減らすための調整と考えられます。
-
test/fixedbugs/bug429.go
の削除:- このファイルは、元のコミット
CL 5844051
で追加されたデッドロックテストケースでした。select{}
という無限にブロックするチャネル操作を含むシンプルなプログラムで、デッドロックが検出されることを期待していました。 - 元のコミットが元に戻されたため、このテストケースも不要となり削除されました。
- このファイルは、元のコミット
-
test/golden.out
の変更:test/golden.out
は、Goのテストスイートにおける特定のテストの期待される出力(特にパニックメッセージなど)を記録するファイルです。bug429.go
テストが削除されたため、そのテストに関連する期待出力のエントリ(throw: all goroutines are asleep - deadlock!
)もこのファイルから削除されました。
これらの変更は、Goランタイムのデッドロック検出ロジックが、マルチプロセッサ環境での複雑なスケジューリング挙動とどのように相互作用するかという課題を示しています。デッドロック検出は重要ですが、その実装は慎重に行う必要があり、偽陽性を避けるためのバランスが求められます。
コアとなるコードの変更箇所
src/pkg/runtime/mheap.c
--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -358,9 +358,6 @@ runtime·MHeap_Scavenger(void)\n \n \th = &runtime·mheap;\n \tfor(k=0;; k++) {\n-\t\t// Return to the scheduler in case the rest of the world is deadlocked.\n-\t\truntime·gosched();\n-\n \t\truntime·noteclear(¬e);\n \t\truntime·entersyscall();\n \t\truntime·notetsleep(¬e, tick);\
runtime·gosched();
の呼び出しが削除されました。
src/pkg/runtime/proc.c
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -521,16 +521,6 @@ mnextg(M *m, G *g)\n \t}\n }\n \n-// Check for a deadlock situation.\n-static void\n-checkdeadlock(void) {\n-\tif((scvg == nil && runtime·sched.grunning == 0) ||\n-\t (scvg != nil && runtime·sched.grunning == 1 && runtime·sched.gwait == 0 &&\n-\t (scvg->status == Grunnable || scvg->status == Grunning || scvg->status == Gsyscall))) {\n-\t\truntime·throw(\"all goroutines are asleep - deadlock!\");\n-\t}\n-}\n-\n // Get the next goroutine that m should run.\n // Sched must be locked on entry, is unlocked on exit.\n // Makes sure that at most $GOMAXPROCS g's are\n@@ -580,9 +570,6 @@ top:\n \t\t\t\tcontinue;\n \t\t\t}\n \t\t\truntime·sched.grunning++;\n-\t\t\t// The work could actually have been the sole scavenger\n-\t\t\t// goroutine. Look for deadlock situation.\n-\t\t\tcheckdeadlock();\n \t\t\tschedunlock();\n \t\t\treturn gp;\n \t\t}\n@@ -604,7 +591,11 @@ top:\n \t}\n \n \t// Look for deadlock situation.\n-\tcheckdeadlock();\n+\tif((scvg == nil && runtime·sched.grunning == 0) ||\n+\t (scvg != nil && runtime·sched.grunning == 1 && runtime·sched.gwait == 0 &&\n+\t (scvg->status == Grunning || scvg->status == Gsyscall))) {\n+\t\truntime·throw(\"all goroutines are asleep - deadlock!\");\n+\t}\n \n \tm->nextg = nil;\n \tm->waitnextg = 1;\
checkdeadlock
関数が削除されました。checkdeadlock()
の呼び出しが削除されました。checkdeadlock
関数内のデッドロック検出ロジックがインライン化され、条件式からscvg->status == Grunnable
が削除されました。
test/fixedbugs/bug429.go
--- a/test/fixedbugs/bug429.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// $G $D/$F.go && $L $F.$A && ! ./$A.out || echo BUG: bug429
-
-// Copyright 2012 The Go Authors. All rights reserved.\n-// Use of this source code is governed by a BSD-style\n-// license that can be found in the LICENSE file.\n-\n-// Should print deadlock message, not hang.\n-\n-package main\n-\n-func main() {\n-\tselect{}\n-}\
- ファイル全体が削除されました。
test/golden.out
--- a/test/golden.out
+++ b/test/golden.out
@@ -15,9 +15,6 @@
== fixedbugs/\n \n-=========== fixedbugs/bug429.go\n-throw: all goroutines are asleep - deadlock!\n-\n == bugs/\n \n =========== bugs/bug395.go\
bug429.go
に関連する期待出力のエントリが削除されました。
コアとなるコードの解説
src/pkg/runtime/mheap.c
における runtime·gosched()
の削除
runtime·MHeap_Scavenger
は、Goのガベージコレクタの一部であるスカベンジャーゴルーチンが実行する関数です。この関数は、メモリヒープの整理や未使用メモリの回収といったバックグラウンドタスクをループで実行します。
元のコミットでは、このループ内に runtime·gosched()
が挿入されていました。runtime·gosched()
は、現在のゴルーチン(この場合はスカベンジャーゴルーチン)の実行を一時停止し、Goスケジューラに制御を戻すことで、他の実行可能なゴルーチンにCPUを譲る役割を果たします。この呼び出しの意図は、スカベンジャーゴルーチンが長時間CPUを占有するのを防ぎ、他のアプリケーションゴルーチンが実行される機会を確保すること、そしてデッドロック状態の検出を助けることでした。
しかし、このコミットでは runtime·gosched()
が削除されました。これは、マルチプロセッサ環境(GOMAXPROCS=2
以上)でのクロージャテストの失敗が、この runtime·gosched()
の導入によって引き起こされたためと考えられます。特定のタイミングでスカベンジャーがCPUを譲ることで、スケジューリングのタイミングが変わり、クロージャの実行順序や状態に予期せぬ影響を与え、テストが失敗する原因となった可能性があります。この削除により、スカベンジャーゴルーチンはより連続的に自身のタスクを実行するようになります。
src/pkg/runtime/proc.c
におけるデッドロック検出ロジックの変更
src/pkg/runtime/proc.c
はGoランタイムのスケジューラの中核部分を実装しています。
-
checkdeadlock
関数の削除とインライン化: 元のコミットでは、checkdeadlock
というヘルパー関数が導入され、スケジューラの主要なループ内でデッドロックの有無をチェックしていました。この関数は、以下の条件に基づいてデッドロックを判断していました。scvg == nil && runtime·sched.grunning == 0
: スカベンジャーゴルーチンが存在せず、かつ実行中のゴルーチンが0の場合。scvg != nil && runtime·sched.grunning == 1 && runtime·sched.gwait == 0 && (scvg->status == Grunnable || scvg->status == Grunning || scvg->status == Gsyscall)
: スカベンジャーゴルーチンが存在し、実行中のゴルーチンが1つ(それがスカベンジャー自身である可能性が高い)、待機中のゴルーチンが0、かつスカベンジャーゴルーチンが実行可能、実行中、またはシステムコール中のいずれかである場合。 これらの条件が満たされると、runtime·throw("all goroutines are asleep - deadlock!")
を呼び出してパニックを発生させていました。
このコミットでは、
checkdeadlock
関数自体が削除され、そのロジックがtop:
ラベルの直後にインライン化されました。 -
デッドロック検出条件の変更: インライン化されたデッドロック検出の
if
条件式は、元のcheckdeadlock
関数内の条件とほぼ同じですが、重要な違いがあります。 元の条件:(scvg->status == Grunnable || scvg->status == Grunning || scvg->status == Gsyscall)
変更後の条件:(scvg->status == Grunning || scvg->status == Gsyscall)
scvg->status == Grunnable
の部分が削除されました。Grunnable
はゴルーチンが実行可能キューに入っており、CPUが利用可能になり次第実行される準備ができている状態を意味します。- この変更は、スカベンジャーゴルーチンが単に「実行可能」であるという理由だけでデッドロックの条件に含めることをやめたことを意味します。スカベンジャーが実行可能であっても、他のゴルーチンがすべてスリープしている状況で、それが直ちにデッドロックを意味するわけではない、という判断があったのかもしれません。これにより、デッドロック検出の条件がわずかに緩和され、特定のマルチプロセッサ環境での偽陽性や不適切なデッドロック検出を回避することを目的としていると考えられます。
これらの変更は、Goランタイムのデッドロック検出の正確性と、マルチコア環境での安定性を確保するための継続的な調整の一部です。
関連リンク
- 元の変更リスト (CL 5844051): https://golang.org/cl/5844051
- この変更リスト (CL 5924045): https://golang.org/cl/5924045
- 関連するGo Issue #3342: https://golang.org/issue/3342
参考にした情報源リンク
特になし。この解説は、提供されたコミット情報とGoランタイムの一般的な知識に基づいて生成されました。