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

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

このコミットは、Goランタイムにおけるデッドロックの問題を修正するものです。具体的には、starttheworld()関数が一部のP(論理プロセッサ)をM(OSスレッド)と関連付けずに残してしまうことで発生するデッドロックを解消します。この問題は、特にCgoを使用するテストケースで散発的に発生していました。

コミット

commit cb945ba6ba23772336bf02fd2364c3df9e9233e0
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Mar 7 21:39:59 2013 +0400

    runtime: fix deadlock
    The deadlock episodically occurs on misc/cgo/test/TestCthread.
    The problem is that starttheworld() leaves some P's with local work
    without M's. Then all active M's enter into syscalls, but reject to
    wake another M's due to the following check (both in entersyscallblock() and in retake()):
    if(p->runqhead == p->runqtail &&
            runtime·atomicload(&runtime·sched.nmspinning) +
            runtime·atomicload(&runtime·sched.npidle) > 0)
            continue;
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/7424054

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

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

元コミット内容

Goランタイムにおいてデッドロックを修正します。 このデッドロックは、misc/cgo/test/TestCthreadで散発的に発生していました。 問題は、starttheworld()が一部のP(ローカルな作業を持つ論理プロセッサ)をM(OSスレッド)なしで残してしまうことでした。その後、全てのアクティブなMがシステムコールに入りますが、entersyscallblock()retake()の両方にある以下のチェックのために、他のMを起動することを拒否します。

if(p->runqhead == p->runqtail &&
        runtime·atomicload(&runtime·sched.nmspinning) +
        runtime·atomicload(&runtime·sched.npidle) > 0)
        continue;

変更の背景

Goランタイムは、ガベージコレクション(GC)などの特定の操作のために、実行中のすべてのゴルーチンを一時停止させる「Stop-the-World (STW)」フェーズを持っています。STWフェーズが終了し、ランタイムが通常の実行を再開する際にstarttheworld()関数が呼び出されます。

このコミットが修正しようとしている問題は、starttheworld()がP(論理プロセッサ)とM(OSスレッド)の関連付けを適切に行えない場合に発生するデッドロックです。具体的には、starttheworld()がPを処理する際に、そのPに割り当てるMが見つからない(mget()nilを返す)状況が発生することがありました。

従来のコードでは、Mが見つからない場合、そのPは単にアイドルPのリストに戻され、ループが中断されていました。これにより、まだ実行すべきゴルーチン(ローカルな作業)を持っているPが、Mと関連付けられないまま放置される可能性がありました。

その後、システム内のすべてのアクティブなMがシステムコール(I/O操作など)に入ると、Goスケジューラは新しいMを起動して、アイドル状態のPや、作業を持つPに割り当てようとします。しかし、コミットメッセージに記載されているチェック(entersyscallblock()retake()内)により、新しいMの起動が抑制されていました。このチェックは、実行キューが空であり、かつスピン中のMやアイドル状態のPが存在する場合に、Mの起動を避けるための最適化です。

この組み合わせにより、以下のデッドロックシナリオが発生しました。

  1. starttheworld()が、作業を持つPをMなしで残してしまう。
  2. 既存のMがすべてシステムコールに入る。
  3. システムコールから戻る際に、新しいMを起動して作業を持つPに割り当てようとするが、上記のチェックにより起動が拒否される。
  4. 結果として、作業を持つP上のゴルーチンは実行されず、システム全体が停止するデッドロック状態に陥る。

この問題は、特にmisc/cgo/test/TestCthreadのようなCgoを使用するテストで散発的に発生していました。CgoはGoとCの間の相互運用を可能にし、システムコールや外部ライブラリの呼び出しを頻繁に行うため、Mがシステムコールに入る機会が増え、このデッドロックの発生確率が高まったと考えられます。

前提知識の解説

このコミットの理解には、GoランタイムのスケジューラにおけるG-M-Pモデルの理解が不可欠です。

  • G (Goroutine): Goにおける軽量な実行単位です。OSスレッドよりもはるかに軽量で、数百万個を同時に実行できます。Goのプログラムは、複数のゴルーチンが並行して動作することで構成されます。
  • M (Machine/OS Thread): オペレーティングシステムのスレッドです。Goランタイムは、MをOSに要求し、そのM上でGを実行します。Mは、システムコールを実行したり、Cgoのコードを実行したりする際にOSにブロックされる可能性があります。
  • P (Processor/Logical Processor): 論理プロセッサ、またはコンテキストです。PはMとGの間の仲介役として機能します。各Pはローカルな実行キューを持ち、そのPに割り当てられたMは、そのPのローカルキューからGを取り出して実行します。Pの数はGOMAXPROCS環境変数によって制御され、通常はCPUコア数に設定されます。Pは、Mがシステムコールに入った際に、他のMに引き渡されることがあります(hand-off)。

Goスケジューラの動作概要: Goスケジューラは、GをM上で実行するためにPを利用します。

  1. MはPと関連付けられ、Pのローカル実行キューからGを取り出して実行します。
  2. Pのローカルキューが空の場合、Mは他のPのキューからGを盗もうとします(work stealing)。
  3. Mがシステムコールに入ると、そのMはPから切り離され、Pは別のMに引き渡されるか、アイドル状態になります。
  4. システムコールから戻ったMは、利用可能なPを探し、見つかればそのPと関連付けられてGの実行を再開します。Pが見つからない場合、Mはアイドル状態になるか、新しいPが利用可能になるまでスピンします。

starttheworld(): この関数は、Goランタイムが「Stop-the-World」フェーズ(例えば、ガベージコレクション中)から復帰する際に呼び出されます。STWフェーズでは、すべてのゴルーチンの実行が停止されます。starttheworld()の目的は、停止していたすべてのPとMを再起動し、ゴルーチンの実行を再開することです。このプロセスには、アイドル状態のPをMと関連付け、必要に応じて新しいMを起動することが含まれます。

entersyscallblock()retake():

  • entersyscallblock(): ゴルーチンがシステムコールに入る直前に呼び出されます。この関数は、現在のMをPから切り離し、Pをアイドル状態にするか、別のMに引き渡します。
  • retake(): システムコールから戻ったMが、実行を再開するためにPを再取得しようとする際に呼び出されます。

コミットメッセージに記載されているチェックは、これらの関数内で、新しいMを起動する必要があるかどうかを判断するために使用されます。

  • p->runqhead == p->runqtail: 現在のPのローカル実行キューが空であることを示します。
  • runtime·atomicload(&runtime·sched.nmspinning): スピン中のMの数。スピン中のMは、作業を探しているがまだ見つかっていないMです。
  • runtime·atomicload(&runtime·sched.npidle): アイドル状態のPの数。

このチェックは、「もし現在のPの実行キューが空で、かつスピン中のMやアイドル状態のPが既に存在する場合、新しいMを起動する必要はない」という最適化の意図があります。しかし、この最適化が、作業を持つPがMと関連付けられないまま放置されるシナリオでデッドロックを引き起こしていました。

技術的詳細

このデッドロックは、starttheworld()関数がPとMの関連付けを完了する前に、利用可能なMが不足し、かつ既存のMがシステムコールに入ってしまうことで発生します。

starttheworld()の元の実装では、pidleget()(アイドルPのリストからPを取得する関数)でPを取得し、そのPに割り当てるMをmget()(利用可能なMを取得する関数)で探します。 もしmget()nilを返した場合(利用可能なMがない場合)、元のコードはpidleput(p)でPをアイドルリストに戻し、ループをbreakしていました。

// 元のコードの一部
while(p = pidleget()) {
    // ...
    mp = mget();
    if(mp == nil) {
        pidleput(p); // Pをアイドルリストに戻す
        break;       // ループを中断
    }
    // ...
}

このbreakが問題でした。starttheworld()は、すべてのPがMと関連付けられることを保証する必要があります。しかし、Mが一時的に不足している場合、このbreakによって、まだ処理されていない(ローカルキューにゴルーチンを持つ可能性のある)Pが残されてしまう可能性がありました。これらのPは、Mと関連付けられないため、その上のゴルーチンは実行されません。

その後、システム内の他のMがすべてシステムコールに入ると、それらのMはPを解放します。システムコールから戻ったMは、新しいMを起動してPに割り当てようとしますが、コミットメッセージに示されたチェック(if(p->runqhead == p->runqtail && runtime·atomicload(&runtime·sched.nmspinning) + runtime·atomicload(&runtime·sched.npidle) > 0) continue;)により、新しいMの起動が抑制されます。このチェックは、Pの実行キューが空であり、かつスピン中のMやアイドルPが存在する場合に、Mの起動を避けるためのものです。しかし、このシナリオでは、Pの実行キューは空ではない(ローカルに作業がある)にもかかわらず、Mが不足しているためにデッドロックが発生します。

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

変更はsrc/pkg/runtime/proc.cファイルのruntime·starttheworld関数に集中しています。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -392,7 +392,7 @@ mhelpgc(void)\n void\n runtime·starttheworld(void)\n {\n-	P *p;\n+	P *p, *p1;\n \tM *mp;\n \tbool add;\n \n@@ -405,6 +405,7 @@ runtime·starttheworld(void)\n \t\tprocresize(runtime·gomaxprocs);\n \truntime·gcwaiting = 0;\n \n+\tp1 = nil;\n \twhile(p = pidleget()) {\n \t\t// procresize() puts p's with work at the beginning of the list.\n \t\t// Once we reach a p without a run queue, the rest don't have one either.\n@@ -414,8 +415,9 @@ runtime·starttheworld(void)\n \t\t}\n \t\tmp = mget();\n \t\tif(mp == nil) {\n-\t\t\tpidleput(p);\n-\t\t\tbreak;\n+\t\t\tp->link = p1;\n+\t\t\tp1 = p;\n+\t\t\tcontinue;\n \t\t}\n \t\tif(mp->nextp)\n \t\t\truntime·throw(\"starttheworld: inconsistent mp->nextp\");\n@@ -428,6 +430,13 @@ runtime·starttheworld(void)\n \t}\n \truntime·unlock(&runtime·sched);\n \n+\twhile(p1) {\n+\t\tp = p1;\n+\t\tp1 = p1->link;\n+\t\tadd = false;\n+\t\tnewm(nil, p);\n+\t}\n+\n \tif(add) {\n \t\t// If GC could have used another helper proc, start one now,\n \t\t// in the hope that it will be available next time.\n```

## コアとなるコードの解説

変更の核心は、`starttheworld()`関数内でMが利用できない場合のPの処理方法にあります。

1.  **新しい変数 `p1` の導入**:
    `P *p, *p1;`
    `p1`という新しい`P`型のポインタが導入されました。これは、一時的にMと関連付けられなかったPを連結リストとして保持するために使用されます。初期値は`nil`です。

2.  **Mが利用できない場合のPの処理変更**:
    元のコードでは、`mget()`が`nil`を返した場合(利用可能なMがない場合)、`pidleput(p)`でPをアイドルリストに戻し、`break`でループを中断していました。
    修正後のコードでは、`break`の代わりに以下の処理が行われます。
    ```c
    if(mp == nil) {
        p->link = p1; // 現在のPをp1が指すリストの先頭に追加
        p1 = p;       // p1を現在のPに更新
        continue;     // 次のPの処理へ
    }
    ```
    これにより、Mと関連付けられなかったPは、即座にアイドルリストに戻されるのではなく、`p1`をヘッドとする単方向連結リストに一時的に保存されます。そして、ループは中断されずに次のPの処理へと`continue`します。これにより、`starttheworld()`は、Mが見つからなくても、すべてのPを一度は処理し続けることができます。

3.  **一時リストに保存されたPの再処理**:
    メインの`while`ループが終了した後、新しい`while`ループが追加されました。
    ```c
    while(p1) {
        p = p1;
        p1 = p1->link;
        add = false; // この行は、GCヘルパープロセスの起動ロジックに影響を与えないようにするため
        newm(nil, p); // 新しいMを起動し、このPと関連付ける
    }
    ```
    このループは、`p1`が`nil`になるまで(つまり、一時リストにPがなくなるまで)実行されます。ループ内で、一時リストからPを一つずつ取り出し、`newm(nil, p)`を呼び出します。
    `newm(nil, p)`は、新しいM(OSスレッド)を起動し、引数で渡されたPと関連付ける関数です。これにより、以前Mと関連付けられなかったすべてのPに対して、新しいMが確実に起動され、そのPと関連付けられるようになります。

この変更により、`starttheworld()`は、Mが一時的に不足している場合でも、すべてのPが最終的にMと関連付けられることを保証できるようになりました。これにより、作業を持つPがMなしで放置され、その結果としてデッドロックが発生するシナリオが解消されます。

## 関連リンク

*   Goランタイムスケジューラに関する公式ドキュメントやブログ記事:
    *   [Go's work-stealing scheduler](https://go.dev/blog/go11-work-stealing) (Go 1.1のスケジューラに関するブログ記事)
    *   [Go scheduler: M, P, G](https://medium.com/a-journey-with-go/go-scheduler-m-p-g-658b07026d0c) (GoスケジューラのM, P, Gに関する解説記事)
*   GoのIssueトラッカー:
    *   [Issue 4993: runtime: deadlock in TestCthread](https://github.com/golang/go/issues/4993) (このコミットに関連する可能性のあるIssue)

## 参考にした情報源リンク

*   Go言語のソースコード (`src/pkg/runtime/proc.c`)
*   Go言語のコミット履歴
*   Go言語の公式ブログ
*   Goスケジューラに関する技術ブログ記事 (例: Medium, Go's official blog)
*   GoのIssueトラッカー (GitHub Issues)
*   Goのコードレビューシステム (Gerrit): [https://golang.org/cl/7424054](https://golang.org/cl/7424054) (コミットメッセージに記載されているChange-ID)
# [インデックス 15630] ファイルの概要

このコミットは、Goランタイムにおけるデッドロックの問題を修正するものです。具体的には、ガベージコレクション後の「Stop-the-World」フェーズ解除時に呼び出される`starttheworld()`関数が、一部のP(論理プロセッサ)をM(OSスレッド)と関連付けずに残してしまうことで発生するデッドロックを解消します。この問題は、特にCgoを使用するテストケース(`misc/cgo/test/TestCthread`)で散発的に発生していました。

## コミット

commit cb945ba6ba23772336bf02fd2364c3df9e9233e0 Author: Dmitriy Vyukov dvyukov@google.com Date: Thu Mar 7 21:39:59 2013 +0400

runtime: fix deadlock
The deadlock episodically occurs on misc/cgo/test/TestCthread.
The problem is that starttheworld() leaves some P's with local work
without M's. Then all active M's enter into syscalls, but reject to
wake another M's due to the following check (both in entersyscallblock() and in retake()):
if(p->runqhead == p->runqtail &&
        runtime·atomicload(&runtime·sched.nmspinning) +
        runtime·atomicload(&runtime·sched.npidle) > 0)
        continue;

R=rsc
CC=golang-dev
https://golang.org/cl/7424054

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

[https://github.com/golang/go/commit/cb945ba6ba23772336bf02fd2364c3df9e9233e0](https://github.com/golang/go/commit/cb945ba6ba23772336bf02fd2364c3df9e9233e0)

## 元コミット内容

Goランタイムにおけるデッドロックを修正します。
このデッドロックは、`misc/cgo/test/TestCthread`で散発的に発生していました。
問題は、`starttheworld()`が一部のP(ローカルな作業を持つ論理プロセッサ)をM(OSスレッド)なしで残してしまうことでした。その後、全てのアクティブなMがシステムコールに入りますが、`entersyscallblock()`と`retake()`の両方にある以下のチェックのために、他のMを起動することを拒否します。

```c
if(p->runqhead == p->runqtail &&
        runtime·atomicload(&runtime·sched.nmspinning) +
        runtime·atomicload(&runtime·sched.npidle) > 0)
        continue;

変更の背景

Goランタイムは、ガベージコレクション(GC)などの特定の操作のために、実行中のすべてのゴルーチンを一時停止させる「Stop-the-World (STW)」フェーズを持っています。STWフェーズが終了し、ランタイムが通常の実行を再開する際にstarttheworld()関数が呼び出されます。この関数は、停止していたすべてのP(論理プロセッサ)とM(OSスレッド)を再起動し、ゴルーチンの実行を再開する役割を担います。

このコミットが修正しようとしている問題は、starttheworld()がPとMの関連付けを適切に行えない場合に発生するデッドロックです。具体的には、starttheworld()がPを処理する際に、そのPに割り当てるMが見つからない(mget()nilを返す)状況が発生することがありました。

従来のコードでは、Mが見つからない場合、そのPは単にアイドルPのリストに戻され、ループが中断されていました。これにより、まだ実行すべきゴルーチン(ローカルな作業)を持っているPが、Mと関連付けられないまま放置される可能性がありました。

その後、システム内のすべてのアクティブなMがシステムコール(I/O操作など)に入ると、Goスケジューラは新しいMを起動して、アイドル状態のPや、作業を持つPに割り当てようとします。しかし、コミットメッセージに記載されているチェック(entersyscallblock()retake()内)により、新しいMの起動が抑制されていました。このチェックは、Pの実行キューが空であり、かつスピン中のMやアイドル状態のPが存在する場合に、Mの起動を避けるための最適化です。

この組み合わせにより、以下のデッドロックシナリオが発生しました。

  1. starttheworld()が、作業を持つPをMなしで残してしまう。
  2. 既存のMがすべてシステムコールに入る。
  3. システムコールから戻る際に、新しいMを起動して作業を持つPに割り当てようとするが、上記のチェックにより起動が拒否される。
  4. 結果として、作業を持つP上のゴルーチンは実行されず、システム全体が停止するデッドロック状態に陥る。

この問題は、特にmisc/cgo/test/TestCthreadのようなCgoを使用するテストで散発的に発生していました。CgoはGoとCの間の相互運用を可能にし、システムコールや外部ライブラリの呼び出しを頻繁に行うため、Mがシステムコールに入る機会が増え、このデッドロックの発生確率が高まったと考えられます。

前提知識の解説

このコミットの理解には、GoランタイムのスケジューラにおけるG-M-Pモデルの理解が不可欠です。

  • G (Goroutine): Goにおける軽量な実行単位です。OSスレッドよりもはるかに軽量で、数百万個を同時に実行できます。Goのプログラムは、複数のゴルーチンが並行して動作することで構成されます。
  • M (Machine/OS Thread): オペレーティングシステムのスレッドです。Goランタイムは、MをOSに要求し、そのM上でGを実行します。Mは、システムコールを実行したり、Cgoのコードを実行したりする際にOSにブロックされる可能性があります。
  • P (Processor/Logical Processor): 論理プロセッサ、またはコンテキストです。PはMとGの間の仲介役として機能します。各Pはローカルな実行キューを持ち、そのPに割り当てられたMは、そのPのローカルキューからGを取り出して実行します。Pの数はGOMAXPROCS環境変数によって制御され、通常はCPUコア数に設定されます。Pは、Mがシステムコールに入った際に、他のMに引き渡されることがあります(hand-off)。

Goスケジューラの動作概要: Goスケジューラは、GをM上で実行するためにPを利用します。

  1. MはPと関連付けられ、Pのローカル実行キューからGを取り出して実行します。
  2. Pのローカルキューが空の場合、Mは他のPのキューからGを盗もうとします(work stealing)。
  3. Mがシステムコールに入ると、そのMはPから切り離され、Pは別のMに引き渡されるか、アイドル状態になります。
  4. システムコールから戻ったMは、利用可能なPを探し、見つかればそのPと関連付けられてGの実行を再開します。Pが見つからない場合、Mはアイドル状態になるか、新しいPが利用可能になるまでスピンします。

starttheworld(): この関数は、Goランタイムが「Stop-the-World」フェーズ(例えば、ガベージコレクション中)から復帰する際に呼び出されます。STWフェーズでは、すべてのゴルーチンの実行が停止されます。starttheworld()の目的は、停止していたすべてのPとMを再起動し、ゴルーチンの実行を再開することです。このプロセスには、アイドル状態のPをMと関連付け、必要に応じて新しいMを起動することが含まれます。

entersyscallblock()retake():

  • entersyscallblock(): ゴルーチンがシステムコールに入る直前に呼び出されます。この関数は、現在のMをPから切り離し、Pをアイドル状態にするか、別のMに引き渡します。
  • retake(): システムコールから戻ったMが、実行を再開するためにPを再取得しようとする際に呼び出されます。

コミットメッセージに記載されているチェックは、これらの関数内で、新しいMを起動する必要があるかどうかを判断するために使用されます。

  • p->runqhead == p->runqtail: 現在のPのローカル実行キューが空であることを示します。
  • runtime·atomicload(&runtime·sched.nmspinning): スピン中のMの数。スピン中のMは、作業を探しているがまだ見つかっていないMです。
  • runtime·atomicload(&runtime·sched.npidle): アイドル状態のPの数。

このチェックは、「もし現在のPの実行キューが空で、かつスピン中のMやアイドル状態のPが既に存在する場合、新しいMを起動する必要はない」という最適化の意図があります。しかし、この最適化が、作業を持つPがMと関連付けられないまま放置されるシナリオでデッドロックを引き起こしていました。

技術的詳細

このデッドロックは、starttheworld()関数がPとMの関連付けを完了する前に、利用可能なMが不足し、かつ既存のMがシステムコールに入ってしまうことで発生します。

starttheworld()の元の実装では、pidleget()(アイドルPのリストからPを取得する関数)でPを取得し、そのPに割り当てるMをmget()(利用可能なMを取得する関数)で探します。 もしmget()nilを返した場合(利用可能なMがない場合)、元のコードはpidleput(p)でPをアイドルリストに戻し、ループをbreakしていました。

// 元のコードの一部
while(p = pidleget()) {
    // ...
    mp = mget();
    if(mp == nil) {
        pidleput(p); // Pをアイドルリストに戻す
        break;       // ループを中断
    }
    // ...
}

このbreakが問題でした。starttheworld()は、すべてのPがMと関連付けられることを保証する必要があります。しかし、Mが一時的に不足している場合、このbreakによって、まだ処理されていない(ローカルキューにゴルーチンを持つ可能性のある)Pが残されてしまう可能性がありました。これらのPは、Mと関連付けられないため、その上のゴルーチンは実行されません。

その後、システム内の他のMがすべてシステムコールに入ると、それらのMはPを解放します。システムコールから戻ったMは、新しいMを起動してPに割り当てようとしますが、コミットメッセージに示されたチェック(if(p->runqhead == p->runqtail && runtime·atomicload(&runtime·sched.nmspinning) + runtime·atomicload(&runtime·sched.npidle) > 0) continue;)により、新しいMの起動が抑制されます。このチェックは、Pの実行キューが空であり、かつスピン中のMやアイドルPが存在する場合に、Mの起動を避けるためのものです。しかし、このシナリオでは、Pの実行キューは空ではない(ローカルに作業がある)にもかかわらず、Mが不足しているためにデッドロックが発生します。

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

変更はsrc/pkg/runtime/proc.cファイルのruntime·starttheworld関数に集中しています。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -392,7 +392,7 @@ mhelpgc(void)\n void\n runtime·starttheworld(void)\n {\n-	P *p;\n+	P *p, *p1;\n \tM *mp;\n \tbool add;\n \n@@ -405,6 +405,7 @@ runtime·starttheworld(void)\n \t\tprocresize(runtime·gomaxprocs);\n \truntime·gcwaiting = 0;\n \n+\tp1 = nil;\n \twhile(p = pidleget()) {\n \t\t// procresize() puts p's with work at the beginning of the list.\n \t\t// Once we reach a p without a run queue, the rest don't have one either.\n@@ -414,8 +415,9 @@ runtime·starttheworld(void)\n \t\t}\n \t\tmp = mget();\n \t\tif(mp == nil) {\n-\t\t\tpidleput(p);\n-\t\t\tbreak;\n+\t\t\tp->link = p1;\n+\t\t\tp1 = p;\n+\t\t\tcontinue;\n \t\t}\n \t\tif(mp->nextp)\n \t\t\truntime·throw(\"starttheworld: inconsistent mp->nextp\");\n@@ -428,6 +430,13 @@ runtime·starttheworld(void)\n \t}\n \truntime·unlock(&runtime·sched);\n \n+\twhile(p1) {\n+\t\tp = p1;\n+\t\tp1 = p1->link;\n+\t\tadd = false;\n+\t\tnewm(nil, p);\n+\t}\n+\n \tif(add) {\n \t\t// If GC could have used another helper proc, start one now,\n \t\t// in the hope that it will be available next time.\n```

## コアとなるコードの解説

変更の核心は、`starttheworld()`関数内でMが利用できない場合のPの処理方法にあります。

1.  **新しい変数 `p1` の導入**:
    `P *p, *p1;`
    `p1`という新しい`P`型のポインタが導入されました。これは、一時的にMと関連付けられなかったPを連結リストとして保持するために使用されます。初期値は`nil`です。

2.  **Mが利用できない場合のPの処理変更**:
    元のコードでは、`mget()`が`nil`を返した場合(利用可能なMがない場合)、`pidleput(p)`でPをアイドルリストに戻し、`break`でループを中断していました。
    修正後のコードでは、`break`の代わりに以下の処理が行われます。
    ```c
    if(mp == nil) {
        p->link = p1; // 現在のPをp1が指すリストの先頭に追加
        p1 = p;       // p1を現在のPに更新
        continue;     // 次のPの処理へ
    }
    ```
    これにより、Mと関連付けられなかったPは、即座にアイドルリストに戻されるのではなく、`p1`をヘッドとする単方向連結リストに一時的に保存されます。そして、ループは中断されずに次のPの処理へと`continue`します。これにより、`starttheworld()`は、Mが見つからなくても、すべてのPを一度は処理し続けることができます。

3.  **一時リストに保存されたPの再処理**:
    メインの`while`ループが終了した後、新しい`while`ループが追加されました。
    ```c
    while(p1) {
        p = p1;
        p1 = p1->link;
        add = false; // この行は、GCヘルパープロセスの起動ロジックに影響を与えないようにするため
        newm(nil, p); // 新しいMを起動し、このPと関連付ける
    }
    ```
    このループは、`p1`が`nil`になるまで(つまり、一時リストにPがなくなるまで)実行されます。ループ内で、一時リストからPを一つずつ取り出し、`newm(nil, p)`を呼び出します。
    `newm(nil, p)`は、新しいM(OSスレッド)を起動し、引数で渡されたPと関連付ける関数です。これにより、以前Mと関連付けられなかったすべてのPに対して、新しいMが確実に起動され、そのPと関連付けられるようになります。

この変更により、`starttheworld()`は、Mが一時的に不足している場合でも、すべてのPが最終的にMと関連付けられることを保証できるようになりました。これにより、作業を持つPがMなしで放置され、その結果としてデッドロックが発生するシナリオが解消されます。

## 関連リンク

*   Goランタイムスケジューラに関する公式ドキュメントやブログ記事:
    *   [Go's work-stealing scheduler](https://go.dev/blog/go11-work-stealing) (Go 1.1のスケジューラに関するブログ記事)
    *   [Go scheduler: M, P, G](https://medium.com/a-journey-with-go/go-scheduler-m-p-g-658b07026d0c) (GoスケジューラのM, P, Gに関する解説記事)
*   GoのIssueトラッカー:
    *   [Issue 4993: runtime: deadlock in TestCthread](https://github.com/golang/go/issues/4993) (このコミットに関連する可能性のあるIssue)

## 参考にした情報源リンク

*   Go言語のソースコード (`src/pkg/runtime/proc.c`)
*   Go言語のコミット履歴
*   Go言語の公式ブログ
*   Goスケジューラに関する技術ブログ記事 (例: Medium, Go's official blog)
*   GoのIssueトラッカー (GitHub Issues)
*   Goのコードレビューシステム (Gerrit): [https://golang.org/cl/7424054](https://golang.org/cl/7424054) (コミットメッセージに記載されているChange-ID)
*   Google検索: "Go runtime P M G"