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

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

このコミットは、Goランタイムにおけるruntime.Gosched()の動作、特にOSスレッドにロックされた(runtime.LockOSThread()を使用している)ゴルーチンに関する重要なバグ修正を扱っています。以前のランタイムのスケジューリングロジックには、ロックされたゴルーチンがruntime.Gosched()を呼び出した際に、プロセッサを他のゴルーチンに適切に譲らず、同じゴルーチンが繰り返し実行されてしまうという問題がありました。このコミットは、その問題を解決し、ロックされたゴルーチンに対してもruntime.Gosched()が正しく機能し、スケジューラの進行が保証されるようにします。

コミット

commit a92e11a256d8a527d547a2772992d9d9870fa817
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Feb 20 12:13:04 2013 +0400

    runtime: ensure forward progress of runtime.Gosched() for locked goroutines
    The removed code leads to the situation when M executes the same locked G again
    and again.
    This is https://golang.org/cl/7310096 but with return instead of break
    in the nested switch.
    Fixes #4820.
    
    R=golang-dev, alex.brainman, rsc
    CC=golang-dev
    https://golang.org/cl/7304102

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

https://github.com/golang/go/commit/a92e11a256d8a527d547a2772992d9d9870fa817

元コミット内容

このコミットの元のメッセージは以下の通りです。

runtime: ensure forward progress of runtime.Gosched() for locked goroutines
The removed code leads to the situation when M executes the same locked G again
and again.
This is https://golang.org/cl/7310096 but with return instead of break
in the nested switch.
Fixes #4820.

R=golang-dev, alex.brainman, rsc
CC=golang-dev
https://golang.org/cl/7304102

変更の背景

この変更の背景には、Goランタイムのスケジューラにおける特定のシナリオでの進行保証の問題がありました。具体的には、runtime.LockOSThread()によってOSスレッドにロックされたゴルーチンがruntime.Gosched()を呼び出した際に、スケジューラがそのゴルーチンを繰り返し同じOSスレッド(M)上で再実行してしまうというバグが存在しました。これにより、他のゴルーチンにCPU時間が割り当てられず、実質的にそのロックされたゴルーチンが無限ループに陥るか、他のゴルーチンが飢餓状態になる可能性がありました。

コミットメッセージにある「The removed code leads to the situation when M executes the same locked G again and again.」という記述がこの問題を明確に示しています。この問題は、Issue #4820として報告されており、このコミットはその問題を修正することを目的としています。

前提知識の解説

このコミットを理解するためには、Goランタイムのスケジューラに関する以下の概念を理解しておく必要があります。

  • ゴルーチン (Goroutine): Goにおける軽量な実行単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行できます。Goランタイムがゴルーチンのスケジューリングを管理します。
  • M (Machine/OS Thread): OSスレッドを表します。Goランタイムは、OSスレッド上でゴルーチンを実行します。
  • P (Processor): 論理プロセッサを表します。PはMとゴルーチンを仲介し、Mがゴルーチンを実行するためのコンテキストを提供します。GOMAXPROCS環境変数によってPの数が制御されます。
  • runtime.Gosched(): この関数は、現在のゴルーチンがプロセッサを自発的に放棄し、他のゴルーチンが実行される機会を与えるために使用されます。これにより、協調的なマルチタスクが実現されます。通常、CPUを占有し続ける可能性のある計算量の多いループなどで呼び出され、他のゴルーチンに実行機会を譲ります。
  • runtime.LockOSThread(): この関数は、現在のゴルーチンを現在のOSスレッド(M)にロックします。つまり、このゴルーチンは、このOSスレッド以外では実行されなくなります。これは、特定のOSスレッドのプロパティ(例:CgoコールでOSスレッドローカルストレージを使用する場合、または特定のOS APIを呼び出す場合)に依存する処理を行う際に必要となります。ロックされたゴルーチンは、そのOSスレッドが終了するか、runtime.UnlockOSThread()が呼び出されるまで、そのスレッドにバインドされたままになります。
  • スケジューラの進行 (Forward Progress): スケジューラが、すべての実行可能なタスク(この場合はゴルーチン)に対して、最終的に実行機会を与え、システム全体として処理が進むことを保証する特性です。進行が保証されない場合、一部のタスクが無限に実行されず、飢餓状態に陥る可能性があります。

このコミットが修正しようとしているのは、runtime.Gosched()がロックされたゴルーチンに対して、この「進行保証」を提供できていなかったという点です。

技術的詳細

このコミットの技術的な核心は、Goランタイムのスケジューリングロジック、特にsrc/pkg/runtime/proc.c内のgput関数における変更にあります。

gput関数は、ゴルーチンをスケジューラに「戻す」役割を担っています。これは、ゴルーチンが実行を一時停止したり、ブロックしたり、あるいはruntime.Gosched()を呼び出したりした際に発生します。

変更前のgput関数には、以下のようなコードブロックが存在しました。

// If g is wired, hand it off directly.
if((mp = gp->lockedm) != nil && canaddmcpu()) {
	mnextg(mp, gp);
	return;
}

このコードブロックは、ゴルーチンgpがOSスレッドにロックされている(gp->lockedm != nil)場合、かつ新しいMを追加できる(canaddmcpu())場合に、そのゴルーチンを直接ロックされているM(mp)に再割り当てしようとしていました(mnextg(mp, gp))。

このロジックが問題を引き起こしていました。runtime.Gosched()が呼び出された際、ゴルーチンは一時的に実行を停止し、gputを通じてスケジューラに戻されます。しかし、もしそのゴルーチンがOSスレッドにロックされており、かつcanaddmcpu()が真を返した場合、上記のコードブロックによって、そのゴルーチンはすぐに同じロックされたMに再割り当てされてしまっていました。

これにより、runtime.Gosched()が意図する「プロセッサを他のゴルーチンに譲る」という目的が達成されず、ロックされたゴルーチンが無限に自身を再スケジューリングし、他のゴルーチンが実行される機会を奪ってしまうという飢餓状態が発生していました。

このコミットでは、この問題のあるコードブロック全体をgput関数から削除しています。これにより、ロックされたゴルーチンがruntime.Gosched()を呼び出した際に、他の通常のゴルーチンと同様にスケジューラのキューに入れられ、公平にスケジューリングされるようになります。ロックされたゴルーチンは引き続き特定のOSスレッド上で実行されますが、Goschedが呼び出された際には、そのOSスレッドが他のゴルーチンを実行する機会を得るか、あるいは他のMが他のゴルーチンを実行する機会を得ることで、全体としての進行が保証されます。

また、このコミットでは、この修正が正しく機能することを検証するための新しいテストケースがsrc/pkg/runtime/proc_test.goに追加されています。TestYieldProgressTestYieldLockedProgressは、runtime.Gosched()が呼び出された際に、ゴルーチンが実際に進行するかどうかを検証します。特にTestYieldLockedProgressは、runtime.LockOSThread()を使用するゴルーチンがruntime.Gosched()を呼び出した際にも、進行が保証されることを確認します。

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

src/pkg/runtime/proc.c

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -397,14 +397,6 @@ canaddmcpu(void)
 static void
 gput(G *gp)
 {
-	M *mp;
-
-	// If g is wired, hand it off directly.
-	if((mp = gp->lockedm) != nil && canaddmcpu()) {
-		mnextg(mp, gp);
-		return;
-	}
-
 	// If g is the idle goroutine for an m, hand it off.
 	if(gp->idlem != nil) {
 		if(gp->idlem->idleg != nil) {

src/pkg/runtime/proc_test.go

--- a/src/pkg/runtime/proc_test.go
+++ b/src/pkg/runtime/proc_test.go
@@ -46,6 +46,36 @@ func TestStopTheWorldDeadlock(t *testing.T) {
 	runtime.GOMAXPROCS(maxprocs)
 }
 
+func TestYieldProgress(t *testing.T) {
+	testYieldProgress(t, false)
+}
+
+func TestYieldLockedProgress(t *testing.T) {
+	testYieldProgress(t, true)
+}
+
+func testYieldProgress(t *testing.T, locked bool) {
+	c := make(chan bool)
+	cack := make(chan bool)
+	go func() {
+		if locked {
+			runtime.LockOSThread()
+		}
+		for {
+			select {
+			case <-c:
+				cack <- true
+				return
+			default:
+				runtime.Gosched()
+			}
+		}
+	}()
+	time.Sleep(10 * time.Millisecond)
+	c <- true
+	<-cack
+}
+
 func TestYieldLocked(t *testing.T) {
 	const N = 10
 	c := make(chan bool)

コアとなるコードの解説

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

gput関数から削除されたコードブロックは、ロックされたゴルーチン(gp->lockedm != nil)を特別扱いし、すぐにそのロックされたMに再割り当てしようとするものでした。この「直接引き渡す」ロジックが、runtime.Gosched()が呼び出された際に、ゴルーチンがプロセッサを適切に譲らず、同じゴルーチンが繰り返し実行される原因となっていました。このブロックを削除することで、ロックされたゴルーチンも他のゴルーチンと同様にスケジューラのキューに配置され、公平なスケジューリングの対象となります。これにより、runtime.Gosched()が意図した通りに機能し、進行が保証されるようになります。

src/pkg/runtime/proc_test.go の追加

追加されたテストコードは、runtime.Gosched()がゴルーチンの進行を保証するかどうかを検証します。

  • TestYieldProgress(t *testing.T): runtime.LockOSThread()を使用しない通常のゴルーチンがruntime.Gosched()を呼び出した際に、チャネルを通じて通信が正常に行われ、ゴルーチンがブロックされずに進行することを確認します。
  • TestYieldLockedProgress(t *testing.T): runtime.LockOSThread()を使用してOSスレッドにロックされたゴルーチンがruntime.Gosched()を呼び出した際に、同様にチャネルを通じて通信が正常に行われ、ゴルーチンがブロックされずに進行することを確認します。これがこのコミットの修正が意図する主要な検証ポイントです。

testYieldProgress関数は、ゴルーチン内で無限ループを実行し、その中でruntime.Gosched()を繰り返し呼び出します。メインゴルーチンは一定時間待機した後、チャネルに値を送信し、テストゴルーチンがその値を受け取って終了することを確認します。もしruntime.Gosched()が正しく機能せず、テストゴルーチンが飢餓状態に陥った場合、チャネルからの受信がタイムアウトし、テストは失敗します。これにより、runtime.Gosched()が実際に他のゴルーチンに実行機会を与え、進行を保証していることが検証されます。

関連リンク

  • Go Issue #4820: https://github.com/golang/go/issues/4820
    • このコミットが修正したバグの報告です。
  • Go Change List 7310096: https://golang.org/cl/7310096
    • コミットメッセージで参照されている、関連する変更リストです。このコミットは、このCLの修正をベースに、より適切なreturnを使用しています。
  • Go Change List 7304102: https://golang.org/cl/7304102
    • コミットメッセージで参照されている、関連する変更リストです。

参考にした情報源リンク

  • Go Issue #4820: runtime: Gosched() does not make forward progress for locked goroutines
  • Go Change List 7310096: runtime: ensure forward progress of runtime.Gosched() for locked goroutines
  • Go Change List 7304102: runtime: fix deadlock in TestYieldLocked
  • Goのスケジューラに関するドキュメントやブログ記事(一般的なGoのM, P, Gモデル、runtime.Gosched()runtime.LockOSThread()の解説を含む)
    • Goの公式ドキュメントやGoのソースコード自体が最も信頼できる情報源です。
    • Goのスケジューラに関する詳細な解説は、Goのブログや技術記事で多く見られます。例えば、「Go's work-stealing scheduler」などで検索すると良いでしょう。
    • Goのソースコード(特にsrc/runtime/proc.gosrc/runtime/proc.c)は、スケジューラの内部動作を理解する上で不可欠です。