[インデックス 19171] ファイルの概要
このコミットは、Goランタイムにおける runtime.Goexit
の挙動、特に main
ゴルーチンが Goexit
を呼び出し、かつ他の全てのゴルーチンが終了した場合のプログラムの振る舞いを修正するものです。
変更されたファイルは以下の通りです。
doc/go1.3.html
: Go 1.3のリリースノートにruntime.Goexit
の新しい挙動に関する記述が追加されました。src/pkg/runtime/crash_test.go
:runtime.Goexit
の挙動をテストするコードが修正され、TestGoexitExit
がTestGoexitCrash
にリネームされ、期待される出力が変更されました。src/pkg/runtime/extern.go
:runtime.Goexit
のドキュメントコメントが更新され、main
ゴルーチンからの呼び出しと他のゴルーチンの終了時の挙動が明記されました。src/pkg/runtime/proc.c
: ランタイムのプロセス管理に関するCコードが修正され、main
ゴルーチンがGoexit
を呼び出した後に他のゴルーチンが全て終了した場合に、プログラムがクラッシュするように変更されました。
コミット
Author: Russ Cox rsc@golang.org Date: Wed Apr 16 13:12:18 2014 -0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ade6bc68b0d71477b3370a20099bcb66de14f517
元コミット内容
runtime: crash when func main calls Goexit and all other goroutines exit
This has typically crashed in the past, although usually with
an 'all goroutines are asleep - deadlock!' message that shows
no goroutines (because there aren't any).
Previous discussion at:
https://groups.google.com/d/msg/golang-nuts/uCT_7WxxopQ/BoSBlLFzUTkJ
https://groups.google.com/d/msg/golang-dev/KUojayEr20I/u4fp_Ej5PdUJ
http://golang.org/issue/7711
There is general agreement that runtime.Goexit terminates the
main goroutine, so that main cannot return, so the program does
not exit.
The interpretation that all other goroutines exiting causes an
exit(0) is relatively new and was not part of those discussions.
That is what this CL changes.
Thankfully, even though the exit(0) has been there for a while,
some other accounting bugs made it very difficult to trigger,
so it is reasonable to replace. In particular, see golang.org/issue/7711#c10
for an examination of the behavior across past releases.
Fixes #7711.
LGTM=iant, r
R=golang-codereviews, iant, dvyukov, r
CC=golang-codereviews
https://golang.org/cl/88210044
変更の背景
このコミットの背景には、Goプログラムにおける runtime.Goexit
の挙動、特に main
ゴルーチンが Goexit
を呼び出した際のプログラムの終了に関する一貫性のない振る舞いがありました。
従来のGoのバージョンでは、main
ゴルーチンが runtime.Goexit
を呼び出し、かつ他の全てのゴルーチンが終了した場合、プログラムがデッドロックとしてクラッシュすることが一般的でした。しかし、一部の自明なケースでは、予期せず正常終了(exit(0)
)してしまうという不整合が存在していました。これは、runtime.Goexit
が main
ゴルーチンを終了させるものの、main
関数自体は戻り値を返さないため、プログラムが終了しないという一般的な理解と矛盾していました。
この不整合は、Goコミュニティ内で議論の対象となっており、特に issue 7711 や関連するメーリングリストのスレッドで活発に議論されていました。議論の結論として、main
ゴルーチンが Goexit
を呼び出した場合、プログラムは常にクラッシュすべきであるという合意が形成されました。
このコミットは、この合意に基づき、main
ゴルーチンが Goexit
を呼び出し、かつ他の全てのゴルーチンが終了した場合に、プログラムが常にデッドロックとしてクラッシュするように修正することで、挙動の一貫性を確保することを目的としています。幸いなことに、以前の exit(0)
の挙動は、他の会計上のバグによりトリガーが非常に困難であったため、この変更は比較的安全に行うことができました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念について理解しておく必要があります。
-
ゴルーチン (Goroutine): Go言語における軽量な実行スレッドです。
go
キーワードを使って関数を呼び出すことで、新しいゴルーチンが生成され、並行して実行されます。ゴルーチンはOSのスレッドよりもはるかに軽量であり、数百万のゴルーチンを同時に実行することも可能です。Goランタイムがゴルーチンのスケジューリングを管理します。 -
main
ゴルーチン: Goプログラムが起動すると、最初にmain
関数が実行されます。このmain
関数を実行するゴルーチンを「main
ゴルーチン」と呼びます。プログラムのライフサイクルにおいて特別な役割を持ちます。 -
runtime.Goexit()
:runtime
パッケージが提供する関数で、これを呼び出したゴルーチンを即座に終了させます。Goexit
を呼び出したゴルーチンは、その後の処理を実行せず、defer
された関数を全て実行してから終了します。重要な点として、Goexit
はパニックとは異なり、呼び出し元の関数から戻り値を返すわけではありません。 -
デッドロック (Deadlock): 並行プログラミングにおいて、複数のゴルーチンが互いに相手の処理の完了を待ち続け、結果としてどのゴルーチンも処理を進められなくなる状態を指します。Goランタイムは、全てのゴルーチンがブロックされ、かつ実行可能なゴルーチンが存在しない場合に、デッドロックを検出し、プログラムをクラッシュさせます。
-
プログラムの終了: Goプログラムは、通常、
main
ゴルーチンがmain
関数の実行を完了するか、os.Exit()
が呼び出されるか、または未処理のパニックが発生した場合に終了します。main
ゴルーチンがruntime.Goexit()
を呼び出した場合、main
関数は戻り値を返さないため、プログラムは他のゴルーチンが実行中であれば継続します。
技術的詳細
このコミットが修正している問題は、main
ゴルーチンが runtime.Goexit()
を呼び出した後、他の全てのゴルーチンが終了した場合のプログラムの振る舞いの不整合です。
runtime.Goexit()
の本来の意図は、呼び出し元のゴルーチンを終了させることです。main
ゴルーチンが Goexit()
を呼び出すと、main
ゴルーチンは終了しますが、main
関数自体は戻り値を返しません。このため、プログラムは直ちに終了するわけではなく、他のゴルーチンが存在すればそれらの実行を継続します。
問題は、その後に他の全てのゴルーチンも終了してしまった場合に発生していました。
Goランタイムには、全てのゴルーチンがスリープ状態(ブロック状態)になり、実行可能なゴルーチンが一つもなくなった場合に、デッドロックと判断してプログラムをクラッシュさせるメカニズムがあります。これは runtime/proc.c
内の checkdead
関数によって行われます。
しかし、このコミット以前のバージョンでは、main
ゴルーチンが Goexit()
を呼び出した後に grunning
(実行中のゴルーチンの数) が0になった場合、runtime·exit(0)
を呼び出して正常終了してしまうという特殊なケースが存在していました。これは、main
ゴルーチンが Goexit
を呼び出した場合でも、プログラムはデッドロックとしてクラッシュすべきであるという一般的な期待と矛盾していました。
このコミットは、この runtime·exit(0)
のパスを削除し、代わりに runtime·throw("no goroutines (main called runtime.Goexit) - deadlock!")
を呼び出すように変更しました。これにより、main
ゴルーチンが Goexit
を呼び出し、かつ他の全てのゴルーチンが終了した場合でも、プログラムは常にデッドロックとしてクラッシュするようになります。これは、main
ゴルーチンが Goexit
を呼び出した時点で main
関数が正常に終了していないため、プログラムが正常終了すべきではないという設計思想に合致します。
また、doc/go1.3.html
と src/pkg/runtime/extern.go
のドキュメントが更新され、この新しい(そして一貫性のある)挙動が明示的に記述されました。テストケース src/pkg/runtime/crash_test.go
も、この変更を反映するように更新されています。
コアとなるコードの変更箇所
このコミットのコアとなるコードの変更は、src/pkg/runtime/proc.c
ファイルにあります。
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -2501,7 +2501,7 @@ checkdead(void)
}\n \truntime·unlock(&allglock);\n \tif(grunning == 0) // possible if main goroutine calls runtime·Goexit()\n-\t\truntime·exit(0);\n+\t\truntime·throw(\"no goroutines (main called runtime.Goexit) - deadlock!\");\n \tm->throwing = -1; // do not dump full stacks\n \truntime·throw(\"all goroutines are asleep - deadlock!\");\n }\n```
また、`src/pkg/runtime/extern.go` の `Goexit` 関数のコメントも更新されています。
```diff
--- a/src/pkg/runtime/extern.go
+++ b/src/pkg/runtime/extern.go
@@ -79,6 +79,11 @@ func Gosched()\n \n // Goexit terminates the goroutine that calls it. No other goroutine is affected.\n // Goexit runs all deferred calls before terminating the goroutine.\n+//\n+// Calling Goexit from the main goroutine terminates that goroutine\n+// without func main returning. Since func main has not returned,\n+// the program continues execution of other goroutines.\n+// If all other goroutines exit, the program crashes.\n func Goexit()\n \n // Caller reports file and line number information about function invocations on
コアとなるコードの解説
src/pkg/runtime/proc.c
の変更は、Goランタイムがデッドロックをチェックする checkdead
関数内で行われています。
変更前のコードでは、grunning == 0
(実行中のゴルーチンが0) かつ main
ゴルーチンが runtime·Goexit()
を呼び出した可能性がある場合に、runtime·exit(0)
を呼び出してプログラムを正常終了させていました。これは、main
ゴルーチンが Goexit
を呼び出したとしても、他のゴルーチンが全て終了すればプログラムは正常終了するという解釈に基づいていました。
しかし、このコミットでは、この runtime·exit(0)
の行が削除され、代わりに runtime·throw("no goroutines (main called runtime.Goexit) - deadlock!")
が挿入されました。
runtime·throw
は、Goランタイムが致命的なエラーを検出した際に呼び出される関数で、指定されたメッセージと共にプログラムをクラッシュさせます。この変更により、main
ゴルーチンが Goexit
を呼び出し、その後に他の全てのゴルーチンが終了した場合、プログラムは明確に「デッドロック」としてクラッシュするようになります。これは、main
関数が正常に終了していない状態でのプログラムの正常終了を防ぎ、より一貫性のある振る舞いを実現します。
src/pkg/runtime/extern.go
の Goexit
関数のコメントの変更は、この新しい挙動を開発者向けに明示的に説明するためのものです。特に、main
ゴルーチンから Goexit
を呼び出した場合のプログラムの継続と、その後に他の全てのゴルーチンが終了した場合のクラッシュについて言及しています。これにより、Goexit
の使用に関する誤解を防ぎ、開発者がより正確なプログラムの振る舞いを予測できるようになります。
関連リンク
- Go Issue 7711: https://golang.org/issue/7711
- golang-nuts メーリングリストの議論: https://groups.google.com/d/msg/golang-nuts/uCT_7WxxopQ/BoSBlLFzUTkJ
- golang-dev メーリングリストの議論: https://groups.google.com/d/msg/golang-dev/KUojayEr20I/u4fp_Ej5PdUJ
- Gerrit Change-ID: https://golang.org/cl/88210044
参考にした情報源リンク
- Go言語公式ドキュメント:
runtime
パッケージ (https://pkg.go.dev/runtime) - Go言語におけるゴルーチンと並行処理に関する一般的な情報源
- Go言語のデッドロック検出メカニズムに関する情報源