[インデックス 19143] ファイルの概要
このコミットは、Goランタイムにおける重要なバグ修正を目的としています。具体的には、main
ゴルーチンが Goexit
を呼び出した際に、プログラムが意図せず早期に終了してしまう問題を解決します。この問題は、ファイナライザ、バックグラウンドスイープ、タイマーといったアイドル状態のバックグラウンドゴルーチンが、プログラムの終了を妨げる「有用な」作業を行っていると誤って認識されていたために発生していました。この修正により、これらのゴルーチンがアイドル状態の際には適切にバックグラウンドとして扱われ、Goexit
のセマンティクスが正しく機能するようになります。
コミット
commit 55e0f36fb46cd3c5b54c4fb0d8444135c3dff0ac
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Apr 15 19:48:17 2014 +0400
runtime: fix program termination when main goroutine calls Goexit
Do not consider idle finalizer/bgsweep/timer goroutines as doing something useful.
We can't simply set isbackground for the whole lifetime of the goroutines,
because when finalizer goroutine calls user function, we do want to consider it
as doing something useful.
This is borken due to timers for quite some time.
With background sweep is become even more broken.
Fixes #7784.
LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/87960044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/55e0f36fb46cd3c5b54c4fb0d8444135c3dff0ac
元コミット内容
このコミットは、main
ゴルーチンが Goexit
を呼び出した際にプログラムが終了する問題を修正します。アイドル状態のファイナライザ、バックグラウンドスイープ、タイマーゴルーチンを「有用な」作業を行っているとは見なさないようにします。これらのゴルーチンに対して、そのライフタイム全体で isbackground
フラグを単純に設定することはできません。なぜなら、ファイナライザゴルーチンがユーザー関数を呼び出す際には、それを「有用な」作業と見なす必要があるためです。この問題はタイマーによって以前から存在していましたが、バックグラウンドスイープの導入によりさらに顕著になりました。Go issue #7784を修正します。
変更の背景
Goプログラムの実行において、main
ゴルーチンが終了すると、通常はプログラム全体も終了します。しかし、runtime.Goexit()
関数は、現在のゴルーチンを終了させますが、他の非デーモンゴルーチンがまだ実行中の場合はプログラム全体を終了させません。このコミット以前には、Goランタイム内部の特定のバックグラウンドゴルーチン(ファイナライザ、GCのバックグラウンドスイープ、タイマー処理を行うゴルーチンなど)が、実際にはアイドル状態であるにもかかわらず、「有用な」作業を行っていると誤って判断され、main
ゴルーチンが Goexit()
を呼び出した際にプログラムが終了しないというバグがありました。
特に、タイマー処理を行うゴルーチンは、長期間アイドル状態になることがあり、この問題の主要な原因となっていました。さらに、バックグラウンドスイープ(GCの一部)が導入されたことで、同様のアイドル状態のゴルーチンが増え、問題がより頻繁に発生するようになりました。このバグは、プログラムが期待通りに終了しないという、ユーザーにとって混乱を招く動作を引き起こしていました。このコミットは、これらのアイドル状態のバックグラウンドゴルーチンを正しく識別し、Goexit()
のセマンティクスが期待通りに機能するようにするために導入されました。
前提知識の解説
runtime.Goexit()
: Go言語の組み込み関数で、現在のゴルーチンを直ちに終了させます。ただし、プログラム全体を終了させるわけではありません。他の非デーモンゴルーチンがまだ実行中であれば、プログラムは継続して実行されます。すべての非デーモンゴルーチンが終了したときにのみ、プログラムは終了します。- ゴルーチン (Goroutines): Goランタイムによって管理される軽量な実行スレッドです。Goプログラムの並行処理の基本単位となります。
- ファイナライザ (Finalizers):
runtime.SetFinalizer
関数を使ってオブジェクトに設定できる関数です。ガベージコレクタがオブジェクトを回収する直前に、そのオブジェクトに対してファイナライザが実行されます。ファイナライザを実行する専用のゴルーチンが存在します。 - バックグラウンドスイープ (Background Sweep): Goのガベージコレクション (GC) の一部です。GCは、メモリを解放するために、使用されなくなったオブジェクトを識別し、回収します。バックグラウンドスイープは、GCのマークフェーズの後に、バックグラウンドでメモリの解放(スイープ)を行うプロセスです。これを行う専用のゴルーチンが存在します。
- タイマー (Timers):
time.After
やtime.NewTimer
などで作成されるタイマーを処理するGoランタイム内部のメカニズムです。タイマーの期限が来たときにチャネルに値を送信したり、関数を実行したりします。タイマーの管理と実行を行う専用のゴルーチンが存在します。 g->isbackground
フラグ: Goランタイムの内部構造体g
(ゴルーチンを表す) に含まれるフラグです。このフラグは、そのゴルーチンがバックグラウンドタスクを実行しているかどうかを示します。このフラグがtrue
の場合、そのゴルーチンはプログラムの終了を妨げる「有用な」作業を行っているとは見なされません。
技術的詳細
このコミットの核心は、Goランタイムがプログラムの終了を判断する際に、どのゴルーチンを「有用な」作業を行っていると見なすか、というロジックの改善にあります。特に runtime.Goexit()
が呼び出された場合、main
ゴルーチンが終了しても、他の非デーモンゴルーチンがアクティブであればプログラムは終了しません。問題は、ファイナライザ、バックグラウンドスイープ、タイマー処理を行うゴルーチンが、アイドル状態であるにもかかわらず、アクティブなゴルーチンとして誤ってカウントされていた点にありました。
修正は、これらのバックグラウンドゴルーチンがアイドル状態(すなわち、次の作業を待って parkunlock
などのランタイム関数でブロックされている状態)に入る直前に、一時的に g->isbackground
フラグを true
に設定し、ブロックから解除された直後に false
に戻すというものです。
これにより、ランタイムはこれらのゴルーチンがアイドル状態にある間は、プログラムの終了を妨げる要因とは見なしません。しかし、ファイナライザゴルーチンがユーザー定義のファイナライザ関数を呼び出すなど、実際にユーザーコードを実行する際には、isbackground
フラグは false
に戻されるため、そのゴルーチンは再び「有用な」作業を行っていると見なされ、プログラムの終了を適切に遅らせることができます。
このアプローチは、isbackground
フラグをゴルーチンのライフタイム全体で固定的に設定するのではなく、ゴルーチンの状態(アイドルかアクティブか)に応じて動的に変更することで、正確なプログラム終了セマンティクスを実現しています。
コアとなるコードの変更箇所
このコミットでは、主に以下の3つのファイルが変更されています。
-
src/pkg/runtime/crash_test.go
:TestGoexitExit
という新しいテストケースが追加されました。このテストは、main
ゴルーチンがGoexit()
を呼び出し、かつファイナライザやタイマーが設定されている状況で、プログラムが正しく終了することを確認します。具体的には、time.Sleep
を含むゴルーチンと、runtime.SetFinalizer
を使用したオブジェクト、そしてruntime.GC()
を呼び出した後にruntime.Goexit()
を呼び出すmain
関数を持つGoプログラムのソースコード (goexitExitSource
) を実行し、出力が空であることを期待します(つまり、プログラムが正常に終了することを確認します)。
-
src/pkg/runtime/mgc0.c
:bgsweep
関数(バックグラウンドスイープ処理)とrunfinq
関数(ファイナライザキューの実行)において、ゴルーチンが次の作業を待ってパーク(ブロック)される直前にg->isbackground = true;
が追加され、パークから解除された直後にg->isbackground = false;
が追加されました。// bgsweep関数内 sweep.parked = true; g->isbackground = true; // 追加 runtime·parkunlock(&gclock, "GC sweep wait"); g->isbackground = false; // 追加 // runfinq関数内 runtime·fingwait = true; g->isbackground = true; // 追加 runtime·parkunlock(&finlock, "finalizer wait"); g->isbackground = false; // 追加
-
src/pkg/runtime/time.goc
:timerproc
関数(タイマー処理)において、ゴルーチンがアイドル状態になり、次のタイマーイベントを待ってパークされる直前にg->isbackground = true;
が追加され、パークから解除された直後にg->isbackground = false;
が追加されました。// timerproc関数内 timers.rescheduling = true; g->isbackground = true; // 追加 runtime·parkunlock(&timers, "timer goroutine (idle)"); g->isbackground = false; // 追加
コアとなるコードの解説
変更の核心は、GoランタイムのCコードにおける g->isbackground
フラグの動的な設定にあります。
-
g->isbackground = true;
: この行は、ファイナライザゴルーチン (runfinq
)、バックグラウンドスイープゴルーチン (bgsweep
)、およびタイマーゴルーチン (timerproc
) が、それぞれruntime·parkunlock
関数を呼び出して自身をパーク(ブロック)する直前に挿入されています。parkunlock
は、ゴルーチンが特定の条件が満たされるまで待機するメカニズムです。このフラグをtrue
に設定することで、ゴルーチンがアイドル状態に入り、次の作業を待っている間は、ランタイムがそのゴルーチンをプログラムの終了を妨げる「有用な」ゴルーチンとは見なさないようにします。 -
g->isbackground = false;
: この行は、runtime·parkunlock
から戻った直後、つまりゴルーチンがブロック状態から解除され、再びアクティブな状態になった直後に挿入されています。これにより、ゴルーチンが実際に作業を再開する際には、再び「有用な」ゴルーチンとして扱われるようになります。特にファイナライザゴルーチンがユーザー定義のファイナライザ関数を実行する際には、このフラグがfalse
であることが重要です。
この動的なフラグの切り替えにより、Goランタイムは、これらのバックグラウンドゴルーチンが実際にユーザーコードを実行している間はプログラムの終了を適切に遅らせ、アイドル状態にある間は Goexit()
によるプログラム終了を妨げないという、正確なセマンティクスを実現しています。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/55e0f36fb46cd3c5b54c4fb0d8444135c3dff0ac
- Go Code Review (CL): https://golang.org/cl/87960044
参考にした情報源リンク
- コミットメッセージの内容
- Go言語のランタイムに関する一般的な知識
- Go言語のガベージコレクションに関する一般的な知識
- Go言語の並行処理に関する一般的な知識