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

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

このコミットは、Goランタイムにおけるデッドロック検出の誤検知(false negative)を回避するための修正です。特に、func main() { select{} }のような自明なデッドロックを検出できない問題を解決することを目的としています。完全な修正ではないものの、一般的なケースでのデッドロック検出の信頼性を向上させます。

コミット

commit 5eb007dedeb59a52a3000202dcb162a3677a384b
Author: Russ Cox <rsc@golang.org>
Date:   Tue Mar 27 12:22:19 2012 -0400

    runtime: work around false negative in deadlock detection
    
    Not a complete fix for issue 3342, but fixes the trivial case.
    There may still be a race in the instants before and after
    a scavenger-induced garbage collection.
    
    Intended to be "obviously safe": a call to runtime·gosched
    before main.main is no different than a call to runtime.Gosched
    at the beginning of main.main, and it is (or had better be)
    safe to call runtime.Gosched at any point during main.
    
    Update #3342.
    
    R=iant
    CC=golang-dev
    https://golang.org/cl/5919052

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

https://github.com/golang/go/commit/5eb007dedeb59a52a3000202dcb162a3677a384b

元コミット内容

runtime: work around false negative in deadlock detection

Not a complete fix for issue 3342, but fixes the trivial case.
There may still be a race in the instants before and after
a scavenger-induced garbage collection.

Intended to be "obviously safe": a call to runtime·gosched
before main.main is no different than a call to runtime.Gosched
at the beginning of main.main, and it is (or had better be)
safe to call runtime.Gosched at any point during main.

Update #3342.

R=iant
CC=golang-dev
https://golang.org/cl/5919052

変更の背景

このコミットは、Goランタイムのデッドロック検出機能における既知の課題、特にIssue 3342に関連する誤検知(false negative)を修正するために導入されました。誤検知とは、実際にはデッドロックが発生しているにもかかわらず、システムがそれを検出できない状態を指します。

具体的な問題は、func main() { select{} } のような非常に単純なプログラムがデッドロックに陥った際に、Goランタイムがそれをデッドロックとして認識せず、ハングアップしてしまうというものでした。これは、Goのスケジューラとガベージコレクタ(特にスカベンジャー)の動作タイミングに起因する競合状態が原因であると考えられました。

コミットメッセージによると、スカベンジャーが起動する直前または直後に競合状態が発生し、デッドロック検出ロジックが正しく機能しない可能性がありました。この修正は、Issue 3342の完全な解決策ではありませんが、少なくともこの自明なケースでのデッドロック検出を確実にすることを目的としています。

開発者は、runtime·gosched()(現在のruntime.Gosched()に相当)の呼び出しをmain.main()の直前に追加することが「明らかに安全」であると考えていました。これは、main.main()の開始時にruntime.Gosched()を呼び出すのと同等であり、main関数内の任意の時点でruntime.Gosched()を呼び出すことは安全であるべきだという前提に基づいています。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念とデッドロックに関する知識が必要です。

  1. Goroutine (ゴルーチン): Goにおける軽量な実行スレッドです。Goプログラムは多数のゴルーチンを並行して実行できます。
  2. Scheduler (スケジューラ): Goランタイムのスケジューラは、多数のゴルーチンを限られた数のOSスレッドにマッピングし、効率的に実行を切り替える役割を担います。これにより、並行処理が実現されます。
  3. Deadlock (デッドロック): 複数のゴルーチンが互いに相手の完了を待機し、結果としてどのゴルーチンも処理を進められなくなる状態です。Goランタイムには、このようなデッドロックを検出する機能が組み込まれています。デッドロックが検出されると、ランタイムはパニックを起こし、エラーメッセージを出力してプログラムを終了させます。
  4. select{}: Goのselectステートメントは、複数の通信操作(チャネルの送受信)を待機するために使用されます。select{}のようにケースが一つも指定されていないselectステートメントは、どのチャネル操作も行われないため、そのゴルーチンは永遠にブロックされます。もしプログラム内のすべてのゴルーチンがこのような状態でブロックされると、デッドロックが発生します。
  5. Garbage Collection (GC) と Scavenger (スカベンジャー): Goは自動メモリ管理(ガベージコレクション)を行います。スカベンジャーは、ガベージコレクションの一部として、不要になったメモリをOSに解放する役割を担うコンポーネントです。スカベンジャーの動作は、ランタイムのスケジューリングやデッドロック検出ロジックと密接に関連しています。
  6. runtime.Gosched(): この関数は、現在のゴルーチンを一時停止し、他のゴルーチンにCPUを譲渡します。これにより、スケジューラが別の実行可能なゴルーチンを選択して実行できるようになります。これは、明示的に協調的なスケジューリングポイントを提供します。
  7. False Negative (誤検知): 統計学やテストの文脈で使われる用語で、実際には陽性(この場合はデッドロック)であるにもかかわらず、システムが陰性(デッドロックではない)と判断してしまうエラーのことです。

技術的詳細

この修正は、src/pkg/runtime/proc.c内のruntime·main関数とデッドロック検出ロジックに焦点を当てています。

Goプログラムのエントリポイントは、C言語で実装されたruntime·main関数です。この関数は、Goランタイムの初期化を行い、最終的にユーザーが記述したmain.main()関数を呼び出します。

デッドロック検出ロジックは、システム内のすべてのゴルーチンがブロックされているかどうかを判断することで機能します。しかし、スカベンジャーの起動タイミングによっては、この検出ロジックが一時的に不正確な状態になる可能性がありました。具体的には、スカベンジャーが起動中であるにもかかわらず、grunning(実行中のゴルーチン数)とgwait(待機中のゴルーチン数)がゼロであると判断され、デッドロックではないと誤認されるケースが存在しました。

コミットメッセージでは、デッドロック検出の条件式について言及しています。 if((scvg == nil && runtime·sched.grunning == 0) || (scvg != nil && runtime·sched.grunning == 1 && runtime·sched.gwait == 0 && (scvg->status == Grunning || scvg->status == Gsyscall))) この条件式は、デッドロックを検出するためのものです。

  • scvg == nil && runtime·sched.grunning == 0: スカベンジャーがまだ起動しておらず、実行中のゴルーチンがない場合。
  • scvg != nil && runtime·sched.grunning == 1 && runtime·sched.gwait == 0 && (scvg->status == Grunning || scvg->status == Gsyscall): スカベンジャーが起動しており、実行中のゴルーチンが1つ(スカベンジャー自身)で、待機中のゴルーチンがなく、スカベンジャーが実行中またはシステムコール中である場合。

問題は、スカベンジャーが起動し始めたばかりの瞬間に、scvg != nil && grunning == 0 && gwait == 0という状態になり、上記の条件式に合致しないため、デッドロックが検出されない可能性があったことです。

この修正は、runtime·main関数内でmain.main()を呼び出す直前にruntime·gosched()を呼び出すことで、この競合状態を緩和します。runtime·gosched()を呼び出すことで、現在のゴルーチン(mainゴルーチン)が一時的にCPUを解放し、スケジューラが他のゴルーチン(特にスカベンジャー)に実行を譲る機会を与えます。これにより、スカベンジャーが完全に起動し、デッドロック検出ロジックがより正確な状態を認識できるようになることが期待されます。

テストケースtest/fixedbugs/bug429.goは、func main() { select{} }というプログラムがデッドロックを正しく検出することを確認するために追加されました。このテストは、プログラムがハングアップするのではなく、「all goroutines are asleep - deadlock!」というメッセージを出力して終了することを確認します。

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

変更は主にsrc/pkg/runtime/proc.cファイルにあります。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -236,6 +236,11 @@ runtime·main(void)
 	if(!runtime·sched.lockmain)
 		runtime·UnlockOSThread();
 
+	// The deadlock detection has false negatives.
+	// Let scvg start up, to eliminate the false negative
+	// for the trivial program func main() { select{} }.
+	runtime·gosched();
+
 	main·main();
 	runtime·exit(0);
 	for(;;)
@@ -591,6 +596,20 @@ top:
 	}
 
 	// Look for deadlock situation.
+	// There is a race with the scavenger that causes false negatives:
+	// if the scavenger is just starting, then we have
+	//		scvg != nil && grunning == 0 && gwait == 0
+	// and we do not detect a deadlock.  It is possible that we should
+	// add that case to the if statement here, but it is too close to Go 1
+	// to make such a subtle change.  Instead, we work around the
+	// false negative in trivial programs by calling runtime.gosched
+	// from the main goroutine just before main.main.
+	// See runtime·main above.
+	//
+	// On a related note, it is also possible that the scvg == nil case is
+	// wrong and should include gwait, but that does not happen in
+	// standard Go programs, which all start the scavenger.
+	//
 	if((scvg == nil && runtime·sched.grunning == 0) ||
 	   (scvg != nil && runtime·sched.grunning == 1 && runtime·sched.gwait == 0 &&
 	    (scvg->status == Grunning || scvg->status == Gsyscall))) {

また、新しいテストファイルtest/fixedbugs/bug429.goが追加されています。

// $G $D/$F.go && $L $F.$A && ! ./$A.out || echo BUG: bug429

// Copyright 2012 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Should print deadlock message, not hang.

package main

func main() {
	select {}
}

そして、test/golden.outにテストの期待される出力が追加されています。

--- a/test/golden.out
+++ b/test/golden.out
@@ -15,6 +15,9 @@
 
 == fixedbugs/
 
+=========== fixedbugs/bug429.go
+throw: all goroutines are asleep - deadlock!
+
 == bugs/
 
 =========== bugs/bug395.go

コアとなるコードの解説

  1. src/pkg/runtime/proc.c の変更:

    • runtime·main関数内でmain.main()を呼び出す直前に、runtime·gosched()の呼び出しが追加されました。
      	// The deadlock detection has false negatives.
      	// Let scvg start up, to eliminate the false negative
      	// for the trivial program func main() { select{} }.
      	runtime·gosched();
      
      	main·main();
      
      この変更により、main.main()が実行される前に、スケジューラが一度実行機会を得て、スカベンジャーなどの他のランタイムゴルーチンが適切に初期化される時間が与えられます。これにより、デッドロック検出ロジックがより安定した状態を認識できるようになります。
    • デッドロック検出ロジックのコメントが更新され、スカベンジャーとの競合状態による誤検知の可能性が明記されました。また、runtime·gosched()による回避策が説明されています。
      	// There is a race with the scavenger that causes false negatives:
      	// if the scavenger is just starting, then we have
      	//		scvg != nil && grunning == 0 && gwait == 0
      	// and we do not detect a deadlock.  It is possible that we should
      	// add that case to the if statement here, but it is too close to Go 1
      	// to make such a subtle change.  Instead, we work around the
      	// false negative in trivial programs by calling runtime.gosched
      	// from the main goroutine just before main.main.
      	// See runtime·main above.
      	//
      	// On a related note, it is also possible that the scvg == nil case is
      	// wrong and should include gwait, but that does not happen in
      	// standard Go programs, which all start the scavenger.
      	//
      
      このコメントは、デッドロック検出の条件式自体を変更するのではなく、runtime·gosched()を呼び出すことで間接的に問題を回避していることを示しています。これは、Go 1のリリースが間近に迫っており、より複雑な変更を避けたかったためと考えられます。
  2. test/fixedbugs/bug429.go の追加:

    • この新しいテストファイルは、func main() { select{} }という最小限のデッドロックプログラムを含んでいます。
    • テストの目的は、このプログラムがハングアップするのではなく、デッドロックメッセージを出力して終了することを確認することです。これは、修正が正しく機能していることを検証するための重要なテストです。
  3. test/golden.out の更新:

    • test/golden.outは、Goのテストスイートにおける期待される出力の「ゴールデンファイル」です。
    • bug429.goのテストが実行された際に、「throw: all goroutines are asleep - deadlock!」というメッセージが出力されることを期待するように更新されました。これにより、テストの自動検証が可能になります。

これらの変更により、Goランタイムはより堅牢なデッドロック検出機能を持つようになり、特に単純なデッドロックケースでの誤検知が減少しました。

関連リンク

参考にした情報源リンク

  • Goのソースコード(特にsrc/pkg/runtime/proc.c
  • GoのIssueトラッカー(Issue 3342)
  • Goのコードレビューシステム(CL 5919052)
  • Goのデッドロック検出に関する一般的なドキュメントや記事(Web検索を通じて得られた情報)
  • Goのスケジューラとガベージコレクションに関する一般的な知識 I have generated the detailed commit explanation as requested.