[インデックス 15590] ファイルの概要
このコミットは、Go言語のランタイムにおけるruntime.Goexit
使用時のデッドロックの誤検知(false positive deadlock)に関するテストを追加するものです。実際のデッドロックの修正は、このコミットよりも前の変更(チェンジリスト cl/7314062
)によって行われており、このコミットはその修正が正しく機能していることを検証するためのテストケースを導入しています。
コミット
commit 2fe840f4f69bb1013ff5ae8968d8ab8257fb2d22
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Mar 5 09:40:17 2013 +0200
runtime: fix false positive deadlock when using runtime.Goexit
Fixes #4893.
Actually it's fixed by cl/7314062 (improved scheduler),
just submitting the test.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7422054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2fe840f4f69bb1013ff5ae8968d8ab8257fb2d22
元コミット内容
このコミットの元々の意図は、runtime.Goexit
を使用する際に発生するデッドロックの誤検知を修正することでした。しかし、コミットメッセージに明記されている通り、この問題の根本的な修正は、スケジューラが改善されたチェンジリスト cl/7314062
によって既に行われています。したがって、このコミット自体は修正コードを含むものではなく、その修正が意図通りに機能していることを確認するための新しいテストケース(TestGoexitDeadlock
)をsrc/pkg/runtime/crash_test.go
に追加するものです。
変更の背景
Go言語のランタイムには、デッドロックを検出するメカニズムが組み込まれています。しかし、特定の状況下、特にruntime.Goexit
が使用されるケースにおいて、実際にはデッドロックが発生していないにもかかわらず、システムがデッドロックと誤って判断してしまう「誤検知(false positive)」の問題が存在していました。この問題はIssue #4893として報告されていました。
この誤検知は、Goのスケジューラがゴルーチン(goroutine)のライフサイクルや状態遷移をどのように管理しているかに起因していました。runtime.Goexit
は現在のゴルーチンを終了させる関数ですが、その終了処理がスケジューラの内部状態と同期する際に、一時的にデッドロックと誤認されるような状況が発生していたと考えられます。
この問題は、cl/7314062
という別のチェンジリストでスケジューラが改善されたことにより、根本的に解決されました。このコミットは、そのスケジューラの改善がデッドロックの誤検知を解消したことを確認するための、具体的なテストケースをGoの標準ライブラリのテストスイートに追加することを目的としています。これにより、将来的な回帰を防ぎ、ランタイムの安定性を保証します。
前提知識の解説
Go言語のゴルーチンとスケジューラ
- ゴルーチン (Goroutine): Go言語における軽量な並行処理の単位です。OSのスレッドよりもはるかに軽量で、数千から数百万のゴルーチンを同時に実行できます。ゴルーチンはGoランタイムによって管理されます。
- Goスケジューラ: Goランタイムに組み込まれたスケジューラは、OSのスレッド(M: Machine)上でゴルーチン(G: Goroutine)を効率的に実行するためのP(Processor)と呼ばれる論理的なプロセッサを管理します。スケジューラは、ゴルーチンの生成、実行、ブロック、再開、そして終了といったライフサイクル全体を制御し、複数のゴルーチンが限られたOSスレッド上で並行して動作できるように調整します。
runtime.Goexit()
runtime.Goexit()
は、呼び出し元のゴルーチンを即座に終了させる関数です。この関数が呼び出されると、現在のゴルーチンはそれ以上実行されず、defer
ステートメントに登録された関数がすべて実行された後、ゴルーチンは終了します。これは、メイン関数や他のゴルーチンが終了するのを待たずに、特定のゴルーチンだけを途中で終了させたい場合に利用されます。ただし、panic
とは異なり、スタックをアンワインドするわけではありません。
デッドロック (Deadlock)
並行プログラミングにおけるデッドロックとは、複数のプロセスやスレッド(Goの場合はゴルーチン)が互いに相手が保持しているリソースの解放を待ち続け、結果としてどのプロセスも処理を進められなくなる状態を指します。Goランタイムには、このようなデッドロックを検出する機能があり、検出された場合はプログラムをクラッシュさせ、デッドロックのスタックトレースを出力します。
誤検知 (False Positive)
デッドロックの誤検知とは、実際にはデッドロックが発生していないにもかかわらず、デッドロック検出メカニズムがデッドロックが発生したと誤って判断してしまう状況を指します。これは、検出ロジックが特定の並行処理パターンをデッドロックと誤解釈したり、ランタイムの内部状態が一時的にデッドロックに似たパターンを示したりする場合に発生します。
チェンジリスト (Change List, CL)
Goプロジェクトでは、Gerritというコードレビューシステムが使われており、変更の単位を「チェンジリスト(CL)」と呼びます。各CLには一意の番号が割り当てられ、関連する変更がまとめられています。コミットメッセージに記載されているcl/7314062
は、このチェンジリストの番号を指し、この番号を元にGerrit上でその変更の詳細を確認できます。
技術的詳細
このコミットは、runtime.Goexit
使用時のデッドロック誤検知という特定の問題に対するテストを追加しています。この問題は、Goスケジューラの内部的な挙動と密接に関連していました。
Goスケジューラは、ゴルーチンがブロックされたり、終了したりする際に、そのゴルーチンが保持していたリソース(例えば、P(論理プロセッサ)やOSスレッド)を解放し、他のゴルーチンに割り当てる処理を行います。runtime.Goexit
が呼び出された際、ゴルーチンは終了状態に入りますが、この終了処理がスケジューラの内部的な同期メカニズムと競合し、一時的にすべてのゴルーチンがブロックされているかのように見え、デッドロック検出器が誤ってデッドロックと判断してしまうことがありました。
cl/7314062
で導入された「改善されたスケジューラ」は、このruntime.Goexit
によるゴルーチン終了時の内部状態遷移をより適切に処理するように変更されました。具体的には、ゴルーチンが終了する際のPの解放や、他のゴルーチンへの切り替えロジックが洗練され、デッドロック検出器が誤作動するような状況が排除されたと考えられます。
このコミットで追加されたテストケースTestGoexitDeadlock
は、まさにこのシナリオを再現するように設計されています。複数のゴルーチンを起動し、そのうちの1つがruntime.Goexit()
を呼び出すというシンプルな構成です。このテストがデッドロックを検出せずに正常に終了することを確認することで、スケジューラの改善がデッドロックの誤検知を解消したことを検証します。テストが期待する出力は空文字列(""
)であり、これはプログラムがクラッシュせずに正常に終了したことを意味します。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/crash_test.go
ファイルに集中しています。
--- a/src/pkg/runtime/crash_test.go
+++ b/src/pkg/runtime/crash_test.go
@@ -91,6 +91,14 @@ func TestLockedDeadlock2(t *testing.T) {
testDeadlock(t, lockedDeadlockSource2)
}
+func TestGoexitDeadlock(t *testing.T) {
+ got := executeTest(t, goexitDeadlockSource, nil)
+ want := ""
+ if got != want {
+ t.Fatalf("expected %q, but got %q", want, got)
+ }
+}
+
const crashSource = `
package main
@@ -175,3 +183,21 @@ func main() {
select {}
}
`
++
+const goexitDeadlockSource = `
++package main
++import (
++ "runtime"
++)
++
++func F() {
++ for i := 0; i < 10; i++ {
++ }
++}
++
++func main() {
++ go F()
++ go F()
++ runtime.Goexit()
++}
++`
具体的には、以下の2つの要素が追加されています。
-
TestGoexitDeadlock
関数:executeTest
ヘルパー関数を使用して、goexitDeadlockSource
という文字列リテラルで定義されたGoプログラムを実行します。- 実行結果が空文字列(
""
)であることを期待します。これは、プログラムがデッドロックでクラッシュせず、正常に終了したことを意味します。 - もし結果が期待と異なる場合、テストは失敗します。
-
goexitDeadlockSource
定数:runtime.Goexit()
を使用するGoプログラムのソースコードを文字列として定義しています。- このプログラムは、
F()
というシンプルなループを持つ関数を2つの異なるゴルーチンで起動し、その後main
ゴルーチンがruntime.Goexit()
を呼び出して終了します。
コアとなるコードの解説
追加されたTestGoexitDeadlock
テストケースは、Goのテストフレームワークと、ランタイムテストで一般的に使用されるexecuteTest
ヘルパー関数を利用しています。
executeTest
関数は、引数として与えられたGoのソースコード文字列をコンパイルし、別のプロセスとして実行します。そして、その実行結果(標準出力やエラー出力)をキャプチャして返します。これにより、テストコード自体がクラッシュすることなく、テスト対象のGoプログラムの挙動(この場合はデッドロックによるクラッシュの有無)を検証できます。
goexitDeadlockSource
で定義されているGoプログラムは非常にシンプルです。
package main
import (
"runtime"
)
func F() {
for i := 0; i < 10; i++ {
}
}
func main() {
go F() // 1つ目のゴルーチンを起動
go F() // 2つ目のゴルーチンを起動
runtime.Goexit() // mainゴルーチンを終了
}
このプログラムの意図は以下の通りです。
main
関数内で、F()
関数を並行して実行する2つの新しいゴルーチンを起動します。F()
関数自体は特に重要な処理を行わず、単に短いループを実行するだけです。- その後、
main
ゴルーチンはruntime.Goexit()
を呼び出して自身を終了させます。
以前のスケジューラでは、このようなシナリオ(特にmain
ゴルーチンがruntime.Goexit()
で終了し、他のゴルーチンがまだ実行中または終了処理中である場合)において、デッドロック検出器が誤ってデッドロックと判断し、プログラムがクラッシュすることがありました。
このテストが成功する(つまり、executeTest
が空文字列を返す)ことは、cl/7314062
で改善されたスケジューラが、runtime.Goexit
によるゴルーチン終了時の内部状態を正しく処理し、デッドロックの誤検知が発生しなくなったことを証明します。これにより、Goランタイムの堅牢性と信頼性が向上したと言えます。
関連リンク
- Go Issue #4893:
runtime.Goexit
使用時のデッドロック誤検知に関する元のIssue。 - チェンジリスト cl/7422054: このコミットに対応するGerritのチェンジリスト。
- チェンジリスト cl/7314062: デッドロックの誤検知を修正した「改善されたスケジューラ」を含むチェンジリスト。
参考にした情報源リンク
- Go言語の公式ドキュメント:
runtime.Goexit
の挙動、ゴルーチンとスケジューラの概念について。 - Go言語のソースコード:
src/pkg/runtime/crash_test.go
および関連するランタイムのコード。 - Gerrit Code Review: Goプロジェクトのチェンジリストの詳細を確認するために使用。
- 並行プログラミングにおけるデッドロックの一般的な概念。
- Go言語のスケジューラに関する技術記事や解説。
- 例: "Go's work-stealing scheduler" (Goのスケジューラに関する詳細な解説記事)
- https://go.dev/blog/go11sched (Go 1.1スケジューラに関する公式ブログ記事)
- https://www.ardanlabs.com/blog/2018/12/scheduling-in-go-part1.html (Goスケジューラに関するArdan Labsの記事)