[インデックス 15276] ファイルの概要
このコミットは、Goランタイムにおけるスケジューリングの挙動、特にruntime.Gosched()
とロックされたゴルーチン(goroutine)のフォワードプログレス(forward progress)に関する重要な修正を含んでいます。具体的には、ロックされたゴルーチンがruntime.Gosched()
を呼び出した際に、同じM(Machine/OSスレッド)上で繰り返し実行され、他のゴルーチンにCPUが渡されないという問題を解決しています。
コミット
commit f87b7f67b232db252a527dbc0005533a27ccb8cd2
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Fri Feb 15 22:22:13 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.
Fixes #4820.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7310096
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f87b7f67b232db252a527dbc000533a27ccb8cd2
元コミット内容
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.
Fixes #4820.
変更の背景
この変更は、Goランタイムのスケジューラにおける特定のバグを修正するために行われました。Goのランタイムは、ゴルーチンと呼ばれる軽量なスレッドを効率的にスケジューリングすることで、高い並行性を実現しています。しかし、特定の条件下、特にruntime.LockOSThread()
によってOSスレッドにロックされたゴルーチンがruntime.Gosched()
を呼び出した際に、スケジューラが適切に機能しない問題がありました。
runtime.Gosched()
は、現在のゴルーチンがCPUを解放し、他のゴルーチンが実行される機会を与えるための関数です。通常、この関数が呼び出されると、現在のゴルーチンは実行キューの末尾に移動し、スケジューラは別の実行可能なゴルーチンを選択して実行します。
しかし、ロックされたゴルーチンの場合、そのゴルーチンは特定のOSスレッドに紐付けられており、そのOSスレッド上でしか実行できません。このコミット以前のランタイムのコードには、gput
関数(ゴルーチンを実行キューに戻す役割を持つ)内で、ロックされたゴルーチンを特別扱いするロジックが存在しました。このロジックは、ロックされたゴルーチンがruntime.Gosched()
を呼び出した際に、そのゴルーチンをすぐに同じM(OSスレッド)に再割り当てしようとするものでした。結果として、ロックされたゴルーチンが無限ループのように同じM上で繰り返し実行され、他のゴルーチンに制御が移らないという「フォワードプログレスの欠如」が発生していました。これは、Issue #4820として報告されていました。
この問題は、特にデッドロックやパフォーマンスの低下を引き起こす可能性があり、ランタイムの安定性と公平なスケジューリングを保証するために修正が必要でした。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。
-
ゴルーチン (Goroutine): Go言語における並行処理の基本単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムのスケジューラによって管理され、複数のOSスレッド(M)上で多重化されて実行されます。
-
M (Machine/OS Thread): Goランタイムがゴルーチンを実行するために使用するOSスレッドです。Goプログラムは、複数のMを生成し、それらのM上でゴルーチンをスケジューリングします。
-
P (Processor): Goランタイムのスケジューラにおける論理的なプロセッサです。Mとゴルーチンの間に位置し、Mがゴルーチンを実行するためのコンテキストを提供します。Pは実行可能なゴルーチンのキューを保持し、MはPからゴルーチンを取得して実行します。
GOMAXPROCS
環境変数によって、同時に実行可能なPの数が制御されます。 -
G (Goroutine): Goランタイム内部でゴルーチンを表す構造体です。
-
runtime.Gosched()
: 現在のゴルーチンを一時停止し、他のゴルーチンにCPUを譲るための関数です。呼び出されたゴルーチンは実行キューに戻され、スケジューラは次に実行すべきゴルーチンを選択します。これにより、協調的なマルチタスクが実現されます。 -
runtime.LockOSThread()
: 現在のゴルーチンを、呼び出し元のOSスレッドにロックするための関数です。この関数が呼び出されると、そのゴルーチンは、そのOSスレッドが終了するまで、他のOSスレッドに移動することなく、常に同じOSスレッド上で実行されます。これは、特定のOSスレッドのプロパティ(例: Cgoコール、GUIライブラリのイベントループなど)に依存する処理を行う場合に必要となります。 -
フォワードプログレス (Forward Progress): 並行システムにおいて、システム全体または個々のタスクが最終的に進行し、完了に向かうことを保証する概念です。デッドロックやライブロックが発生すると、フォワードプログレスが失われます。このコミットの文脈では、
runtime.Gosched()
が呼び出されたにもかかわらず、他のゴルーチンに実行が移らず、同じゴルーチンが繰り返し実行されてしまう状況を指します。 -
proc.c
: Goランタイムのスケジューラの中核部分を実装しているC言語のファイルです。ゴルーチンの生成、スケジューリング、MとPの管理など、低レベルな処理が記述されています。 -
proc_test.go
:proc.c
で実装されたスケジューラの挙動をテストするためのGo言語のテストファイルです。
技術的詳細
このコミットの技術的詳細は、src/pkg/runtime/proc.c
からのコード削除と、src/pkg/runtime/proc_test.go
へのテスト追加に集約されます。
src/pkg/runtime/proc.c
の変更
削除されたコードは、gput
関数内にありました。gput
関数は、ゴルーチン(gp
)を実行可能な状態にしてスケジューラに戻す役割を担っています。
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) {
削除された部分のコメント「If g is wired, hand it off directly.」が示すように、このコードはgp->lockedm
がnil
でない(つまり、ゴルーチンがOSスレッドにロックされている)場合に、そのゴルーチンを直接そのロックされたM(mp
)に再割り当てしようとしていました。canaddmcpu()
は、新しいMを追加できるかどうかをチェックする関数ですが、この文脈では、ロックされたゴルーチンをすぐに再実行させるための条件として使われていました。mnextg(mp, gp)
は、指定されたM(mp
)に指定されたゴルーチン(gp
)を次に実行させるように設定する関数です。
このロジックが問題でした。runtime.Gosched()
が呼び出された際、ゴルーチンはgput
を通じてスケジューラに戻されます。しかし、ロックされたゴルーチンの場合、この削除されたコードパスによって、ゴルーチンはすぐに同じMに再割り当てされてしまい、他のゴルーチンに実行機会が与えられませんでした。これにより、runtime.Gosched()
の意図する「CPUの譲渡」が機能せず、フォワードプログレスが阻害されていました。
このコードを削除することで、ロックされたゴルーチンも他の通常のゴルーチンと同様に、実行キューに適切に戻され、スケジューラの通常のロジックに従ってスケジューリングされるようになりました。これにより、runtime.Gosched()
がロックされたゴルーチンに対しても正しく機能し、フォワードプログレスが保証されるようになりました。
src/pkg/runtime/proc_test.go
の変更
このコミットでは、上記の修正が正しく機能することを検証するための新しいテストケースが追加されています。
+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
+ break
+ default:
+ runtime.Gosched()
+ }
+ }
+ }()
+ time.Sleep(10 * time.Millisecond)
+ c <- true
+ <-cack
+}
TestYieldProgress
: ロックされていない通常のゴルーチンに対するruntime.Gosched()
のフォワードプログレスをテストします。TestYieldLockedProgress
:runtime.LockOSThread()
によってOSスレッドにロックされたゴルーチンに対するruntime.Gosched()
のフォワードプログレスをテストします。これがこのコミットで修正された主要なシナリオです。
両方のテストはtestYieldProgress
関数を呼び出します。
testYieldProgress
関数は以下のロジックで構成されています。
- 2つのチャネル
c
とcack
を作成します。 - 新しいゴルーチンを起動します。
- 新しいゴルーチン内で、
locked
がtrue
の場合(TestYieldLockedProgress
の場合)、runtime.LockOSThread()
を呼び出して現在のゴルーチンをOSスレッドにロックします。 - 無限ループに入り、
select
文を使用します。c
チャネルから値を受信した場合、cack
チャネルにtrue
を送信し、ループを抜けます。default
ケースでは、runtime.Gosched()
を呼び出します。これは、c
チャネルからの受信がない場合に、ゴルーチンがCPUを譲り、他のゴルーチンに実行機会を与えることを意図しています。
- メインのテストゴルーチンは、新しいゴルーチンが起動するのを少し待つために
time.Sleep(10 * time.Millisecond)
を実行します。 - その後、
c <- true
でc
チャネルに値を送信し、新しいゴルーチンに終了を指示します。 - 最後に、
<-cack
でcack
チャネルから値を受信するのを待ちます。これは、新しいゴルーチンがc
チャネルからの値を受信し、正常に終了したことを確認するためのものです。
このテストの目的は、runtime.Gosched()
が呼び出された際に、ゴルーチンが無限にdefault
ケースでGosched
を呼び出し続けることなく、最終的にc
チャネルからの値を受信して終了できることを保証することです。特にTestYieldLockedProgress
では、ロックされたゴルーチンがGosched
を呼び出しても、他のゴルーチン(この場合はメインのテストゴルーチン)がc
チャネルに値を送信できる機会を得られることを確認します。修正前は、ロックされたゴルーチンがGosched
を呼び出しても、すぐに同じMに再割り当てされてしまい、c
チャネルへの送信がブロックされる可能性がありました。このテストがパスすることで、フォワードプログレスが保証されたことになります。
コアとなるコードの変更箇所
削除されたコード (src/pkg/runtime/proc.c
)
- M *mp;
-
- // If g is wired, hand it off directly.
- if((mp = gp->lockedm) != nil && canaddmcpu()) {
- mnextg(mp, gp);
- return;
- }
追加されたコード (src/pkg/runtime/proc_test.go
)
+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
+ break
+ default:
+ runtime.Gosched()
+ }
+ }
+ }()
+ time.Sleep(10 * time.Millisecond)
+ c <- true
+ <-cack
+}
コアとなるコードの解説
削除されたC言語のコードは、gput
関数内でロックされたゴルーチンを特別扱いするロジックでした。このロジックは、ゴルーチンがOSスレッドにロックされている場合(gp->lockedm != nil
)、そのゴルーチンをすぐに同じMに再割り当てしようとしました。これは、runtime.Gosched()
が呼び出された際に、ゴルーチンがCPUを譲るのではなく、すぐに再実行されてしまう原因となっていました。このコードを削除することで、ロックされたゴルーチンも通常のゴルーチンと同様にスケジューリングキューに戻され、スケジューラの公平な選択対象となるようになりました。
追加されたGo言語のテストコードは、この修正が正しく機能することを検証するためのものです。特にTestYieldLockedProgress
は、runtime.LockOSThread()
でロックされたゴルーチンがruntime.Gosched()
を呼び出した後でも、他のゴルーチンがチャネルを通じて通信できることを確認します。これは、ロックされたゴルーチンがCPUを適切に譲り、フォワードプログレスが保証されていることを意味します。テスト内のselect
文とruntime.Gosched()
の組み合わせは、ゴルーチンがチャネルからの受信を待つ間にCPUを譲り、最終的にチャネルからの値を受信して終了できることを確認するための典型的なパターンです。
関連リンク
- Go Issue #4820: https://github.com/golang/go/issues/4820
- Gerrit Change-Id:
7310096
(Goのコードレビューシステム)
参考にした情報源リンク
- Go言語の公式ドキュメント (runtimeパッケージ): https://pkg.go.dev/runtime
- Goスケジューラに関するブログ記事や解説(一般的な情報源)
- "Go's work-stealing scheduler": https://go.dev/blog/go11sched
- "The Go scheduler": https://www.ardanlabs.com/blog/2018/08/go-scheduler.html
- Goのソースコード (特に
src/runtime/proc.go
やsrc/runtime/proc.c
の現在のバージョン) - Goのコミット履歴と関連する議論
- GoのIssueトラッカー (Issue #4820の詳細)
- GoのGerritコードレビューシステム (Change-Id
7310096
の詳細)
[インデックス 15276] ファイルの概要
このコミットは、Goランタイムにおけるスケジューリングの挙動、特にruntime.Gosched()
とロックされたゴルーチン(goroutine)のフォワードプログレス(forward progress)に関する重要な修正を含んでいます。具体的には、ロックされたゴルーチンがruntime.Gosched()
を呼び出した際に、同じM(Machine/OSスレッド)上で繰り返し実行され、他のゴルーチンにCPUが渡されないという問題を解決しています。
コミット
commit f87b7f67b232db252a527dbc000533a27ccb8cd2
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Fri Feb 15 22:22:13 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.
Fixes #4820.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7310096
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f87b7f67b232db252a527dbc000533a27ccb8cd2
元コミット内容
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.
Fixes #4820.
変更の背景
この変更は、Goランタイムのスケジューラにおける特定のバグを修正するために行われました。Goのランタイムは、ゴルーチンと呼ばれる軽量なスレッドを効率的にスケジューリングすることで、高い並行性を実現しています。しかし、特定の条件下、特にruntime.LockOSThread()
によってOSスレッドにロックされたゴルーチンがruntime.Gosched()
を呼び出した際に、スケジューラが適切に機能しない問題がありました。
runtime.Gosched()
は、現在のゴルーチンがCPUを解放し、他のゴルーチンが実行される機会を与えるための関数です。通常、この関数が呼び出されると、現在のゴルーチンは実行キューの末尾に移動し、スケジューラは別の実行可能なゴルーチンを選択して実行します。
しかし、ロックされたゴルーチンの場合、そのゴルーチンは特定のOSスレッドに紐付けられており、そのOSスレッド上でしか実行できません。このコミット以前のランタイムのコードには、gput
関数(ゴルーチンを実行キューに戻す役割を持つ)内で、ロックされたゴルーチンを特別扱いするロジックが存在しました。このロジックは、ロックされたゴルーチンがruntime.Gosched()
を呼び出した際に、そのゴルーチンをすぐに同じM(OSスレッド)に再割り当てしようとするものでした。結果として、ロックされたゴルーチンが無限ループのように同じM上で繰り返し実行され、他のゴルーチンに制御が移らないという「フォワードプログレスの欠如」が発生していました。これは、Go Issue #4820として報告されていました。
この問題は、特にデッドロックやパフォーマンスの低下を引き起こす可能性があり、ランタイムの安定性と公平なスケジューリングを保証するために修正が必要でした。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。
-
ゴルーチン (Goroutine): Go言語における並行処理の基本単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムのスケジューラによって管理され、複数のOSスレッド(M)上で多重化されて実行されます。
-
M (Machine/OS Thread): Goランタイムがゴルーチンを実行するために使用するOSスレッドです。Goプログラムは、複数のMを生成し、それらのM上でゴルーチンをスケジューリングします。
-
P (Processor): Goランタイムのスケジューラにおける論理的なプロセッサです。Mとゴルーチンの間に位置し、Mがゴルーチンを実行するためのコンテキストを提供します。Pは実行可能なゴルーチンのキューを保持し、MはPからゴルーチンを取得して実行します。
GOMAXPROCS
環境変数によって、同時に実行可能なPの数が制御されます。 -
G (Goroutine): Goランタイム内部でゴルーチンを表す構造体です。
-
runtime.Gosched()
: 現在のゴルーチンを一時停止し、他のゴルーチンにCPUを譲るための関数です。呼び出されたゴルーチンは実行キューに戻され、スケジューラは次に実行すべきゴルーチンを選択します。これにより、協調的なマルチタスクが実現されます。 -
runtime.LockOSThread()
: 現在のゴルーチンを、呼び出し元のOSスレッドにロックするための関数です。この関数が呼び出されると、そのゴルーチンは、そのOSスレッドが終了するまで、他のOSスレッドに移動することなく、常に同じOSスレッド上で実行されます。これは、特定のOSスレッドのプロパティ(例: Cgoコール、GUIライブラリのイベントループなど)に依存する処理を行う場合に必要となります。 -
フォワードプログレス (Forward Progress): 並行システムにおいて、システム全体または個々のタスクが最終的に進行し、完了に向かうことを保証する概念です。デッドロックやライブロックが発生すると、フォワードプログレスが失われます。このコミットの文脈では、
runtime.Gosched()
が呼び出されたにもかかわらず、他のゴルーチンに実行が移らず、同じゴルーチンが繰り返し実行されてしまう状況を指します。 -
proc.c
: Goランタイムのスケジューラの中核部分を実装しているC言語のファイルです。ゴルーチンの生成、スケジューリング、MとPの管理など、低レベルな処理が記述されています。 -
proc_test.go
:proc.c
で実装されたスケジューラの挙動をテストするためのGo言語のテストファイルです。
技術的詳細
このコミットの技術的詳細は、src/pkg/runtime/proc.c
からのコード削除と、src/pkg/runtime/proc_test.go
へのテスト追加に集約されます。
src/pkg/runtime/proc.c
の変更
削除されたコードは、gput
関数内にありました。gput
関数は、ゴルーチン(gp
)を実行可能な状態にしてスケジューラに戻す役割を担っています。
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) {
削除された部分のコメント「If g is wired, hand it off directly.」が示すように、このコードはgp->lockedm
がnil
でない(つまり、ゴルーチンがOSスレッドにロックされている)場合に、そのゴルーチンを直接そのロックされたM(mp
)に再割り当てしようとしていました。canaddmcpu()
は、新しいMを追加できるかどうかをチェックする関数ですが、この文脈では、ロックされたゴルーチンをすぐに再実行させるための条件として使われていました。mnextg(mp, gp)
は、指定されたM(mp
)に指定されたゴルーチン(gp
)を次に実行させるように設定する関数です。
このロジックが問題でした。runtime.Gosched()
が呼び出された際、ゴルーチンはgput
を通じてスケジューラに戻されます。しかし、ロックされたゴルーチンの場合、この削除されたコードパスによって、ゴルーチンはすぐに同じMに再割り当てされてしまい、他のゴルーチンに実行機会が与えられませんでした。これにより、runtime.Gosched()
の意図する「CPUの譲渡」が機能せず、フォワードプログレスが阻害されていました。
このコードを削除することで、ロックされたゴルーチンも他の通常のゴルーチンと同様に、実行キューに適切に戻され、スケジューラの通常のロジックに従ってスケジューリングされるようになりました。これにより、runtime.Gosched()
がロックされたゴルーチンに対しても正しく機能し、フォワードプログレスが保証されるようになりました。
src/pkg/runtime/proc_test.go
の変更
このコミットでは、上記の修正が正しく機能することを検証するための新しいテストケースが追加されています。
+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
+ break
+ default:
+ runtime.Gosched()
+ }
+ }
+ }()
+ time.Sleep(10 * time.Millisecond)
+ c <- true
+ <-cack
+}
TestYieldProgress
: ロックされていない通常のゴルーチンに対するruntime.Gosched()
のフォワードプログレスをテストします。TestYieldLockedProgress
:runtime.LockOSThread()
によってOSスレッドにロックされたゴルーチンに対するruntime.Gosched()
のフォワードプログレスをテストします。これがこのコミットで修正された主要なシナリオです。
両方のテストはtestYieldProgress
関数を呼び出します。
testYieldProgress
関数は以下のロジックで構成されています。
- 2つのチャネル
c
とcack
を作成します。 - 新しいゴルーチンを起動します。
- 新しいゴルーチン内で、
locked
がtrue
の場合(TestYieldLockedProgress
の場合)、runtime.LockOSThread()
を呼び出して現在のゴルーチンをOSスレッドにロックします。 - 無限ループに入り、
select
文を使用します。c
チャネルから値を受信した場合、cack
チャネルにtrue
を送信し、ループを抜けます。default
ケースでは、runtime.Gosched()
を呼び出します。これは、c
チャネルからの受信がない場合に、ゴルーチンがCPUを譲り、他のゴルーチンに実行機会を与えることを意図しています。
- メインのテストゴルーチンは、新しいゴルーチンが起動するのを少し待つために
time.Sleep(10 * time.Millisecond)
を実行します。 - その後、
c <- true
でc
チャネルに値を送信し、新しいゴルーチンに終了を指示します。 - 最後に、
<-cack
でcack
チャネルから値を受信するのを待ちます。これは、新しいゴルーチンがc
チャネルからの値を受信し、正常に終了したことを確認するためのものです。
このテストの目的は、runtime.Gosched()
が呼び出された際に、ゴルーチンが無限にdefault
ケースでGosched
を呼び出し続けることなく、最終的にc
チャネルからの値を受信して終了できることを保証することです。特にTestYieldLockedProgress
では、ロックされたゴルーチンがGosched
を呼び出しても、他のゴルーチン(この場合はメインのテストゴルーチン)がc
チャネルに値を送信できる機会を得られることを確認します。修正前は、ロックされたゴルーチンがGosched
を呼び出しても、すぐに同じMに再割り当てされてしまい、c
チャネルへの送信がブロックされる可能性がありました。このテストがパスすることで、フォワードプログレスが保証されたことになります。
コアとなるコードの変更箇所
削除されたコード (src/pkg/runtime/proc.c
)
- M *mp;
-
- // If g is wired, hand it off directly.
- if((mp = gp->lockedm) != nil && canaddmcpu()) {
- mnextg(mp, gp);
- return;
- }
追加されたコード (src/pkg/runtime/proc_test.go
)
+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
+ break
+ default:
+ runtime.Gosched()
+ }
+ }
+ }()
+ time.Sleep(10 * time.Millisecond)
+ c <- true
+ <-cack
+}
コアとなるコードの解説
削除されたC言語のコードは、gput
関数内でロックされたゴルーチンを特別扱いするロジックでした。このロジックは、ゴルーチンがOSスレッドにロックされている場合(gp->lockedm != nil
)、そのゴルーチンをすぐに同じMに再割り当てしようとしました。これは、runtime.Gosched()
が呼び出された際に、ゴルーチンがCPUを譲るのではなく、すぐに再実行されてしまう原因となっていました。このコードを削除することで、ロックされたゴルーチンも通常のゴルーチンと同様にスケジューリングキューに戻され、スケジューラの公平な選択対象となるようになりました。
追加されたGo言語のテストコードは、この修正が正しく機能することを検証するためのものです。特にTestYieldLockedProgress
は、runtime.LockOSThread()
でロックされたゴルーチンがruntime.Gosched()
を呼び出した後でも、他のゴルーチンがチャネルを通じて通信できることを確認します。これは、ロックされたゴルーチンがCPUを適切に譲り、フォワードプログレスが保証されていることを意味します。テスト内のselect
文とruntime.Gosched()
の組み合わせは、ゴルーチンがチャネルからの受信を待つ間にCPUを譲り、最終的にチャネルからの値を受信して終了できることを確認するための典型的なパターンです。
関連リンク
- Go Issue #4820: https://github.com/golang/go/issues/4820
- Gerrit Change-Id:
7310096
(Goのコードレビューシステム)
参考にした情報源リンク
- Go言語の公式ドキュメント (runtimeパッケージ): https://pkg.go.dev/runtime
- Goスケジューラに関するブログ記事や解説(一般的な情報源)
- "Go's work-stealing scheduler": https://go.dev/blog/go11sched
- "The Go scheduler": https://www.ardanlabs.com/blog/2018/08/go-scheduler.html
- Goのソースコード (特に
src/runtime/proc.go
やsrc/runtime/proc.c
の現在のバージョン) - Goのコミット履歴と関連する議論
- GoのIssueトラッカー (Issue #4820の詳細)
- GoのGerritコードレビューシステム (Change-Id
7310096
の詳細)