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

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

このコミットは、Goランタイムのスケジューラ関数の一部において、プリエンプション(横取り)を無効にする変更を導入します。これは、Goのスケジューラが協調的スケジューリングからプリエンプティブ(横取り型)スケジューリングへと進化する過程で必要とされた重要な修正であり、特にスケジューラの内部状態の整合性を保つために行われました。

コミット

commit 4a8ef1f65db072ecd6ff79201338ac75b43640fa
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Jun 3 14:40:38 2013 +0400

    runtime: disable preemption in several scheduler functions
    Required for preemptive scheduler, see the comments for details.
    
    R=golang-dev, khr, iant, khr
    CC=golang-dev
    https://golang.org/cl/9740051

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

https://github.com/golang/go/commit/4a8ef1f65db072ecd6ff79201338ac75b43640fa

元コミット内容

runtime: disable preemption in several scheduler functions Required for preemptive scheduler, see the comments for details.

このコミットは、Goランタイムのいくつかのスケジューラ関数において、プリエンプションを無効にするものです。これはプリエンプティブスケジューラのために必要とされ、詳細はコード内のコメントに記載されています。

変更の背景

Goの初期バージョン(Go 1.0からGo 1.1の一部まで)では、スケジューラは基本的に「協調的(cooperative)」でした。これは、ゴルーチンが自らスケジューラに制御を返す(例えば、チャネル操作、システムコール、明示的なruntime.Gosched()呼び出しなど)まで、CPUを占有し続けることを意味します。この協調的スケジューリングモデルは、シンプルな設計と低いオーバーヘッドという利点がありましたが、いくつかの問題も抱えていました。

主な問題は、CPUバウンドな無限ループや、長時間実行される計算処理を行うゴルーチンが、他のゴルーチンにCPUを明け渡さないために、プログラム全体の応答性を低下させたり、デッドロックを引き起こしたりする可能性があったことです。特に、ガベージコレクション(GC)のようなランタイムの重要な処理が、協調的スケジューリングの制約によって遅延する可能性がありました。

この問題を解決するため、Goチームはより堅牢な「プリエンプティブ(preemptive)」スケジューリングの導入を進めました。プリエンプティブスケジューリングでは、スケジューラが一定時間ごとにゴルーチンの実行を強制的に中断し、別のゴルーチンにCPUを割り当てることができます。これにより、単一のゴルーチンがCPUを独占することを防ぎ、より公平なリソース配分と全体的な応答性の向上を実現します。

しかし、プリエンプティブスケジューリングの導入は、ランタイムの内部、特にスケジューラ自身のコードに複雑性をもたらします。スケジューラが自身の内部状態(例えば、M, P, G構造体間のリンクや、Pのローカル変数への保持など)を操作している最中にプリエンプションが発生すると、データ競合や不整合が生じる可能性があります。このコミットは、まさにそのようなクリティカルセクションにおいて、一時的にプリエンプションを無効にすることで、スケジューラの整合性を保護するために導入されました。

前提知識の解説

Goランタイムスケジューラ (M, P, Gモデル)

Goのスケジューラは、M(Machine/Thread)、P(Processor/Context)、G(Goroutine)という3つの主要な抽象化に基づいて設計されています。

  • G (Goroutine): Goにおける軽量な実行単位です。Goプログラム内で並行して実行される関数やメソッドのインスタンスを表します。各Gは独自のスタックを持ち、スケジューラによってM上で実行されます。
  • M (Machine/Thread): オペレーティングシステム(OS)のスレッドを表します。Goランタイムは、OSスレッドを生成し、その上でGoのコードを実行します。MはGを実行するための物理的なリソースを提供します。
  • P (Processor/Context): MとGを仲介する論理的なプロセッサです。Pは、実行可能なGのキュー(ローカル実行キュー)を保持し、MがGを実行するためのコンテキストを提供します。MはPにアタッチされ、PのローカルキューからGを取得して実行します。これにより、複数のMが同時にGを実行する際に、Gのスケジューリングを効率的に調整します。Pの数は、通常、CPUコアの数に等しく設定され、GOMAXPROCS環境変数で制御できます。

スケジューラの基本的な流れは、MがPにアタッチされ、PのローカルキューからGを取り出して実行します。Gがブロックされる(例: ネットワークI/O待ち)と、MはPを解放し、別のMがそのPにアタッチされて別のGを実行できます。

プリエンプション (Preemption) の概念

プリエンプションとは、実行中のタスク(この場合はゴルーチン)が、自ら制御を放棄することなく、外部のメカニズム(スケジューラ)によって強制的に中断され、別のタスクにCPUの実行権が移されることです。

  • 協調的プリエンプション: タスクが特定のポイント(関数呼び出し、システムコールなど)で自発的にスケジューラに制御を返すことを期待するモデル。Goの初期バージョンでは、関数プロローグでのスタックチェックを利用して、長時間実行されるループでもスケジューラに制御を返す機会を設けていました。
  • 非同期プリエンプション(Async Preemption): 外部からのシグナルやタイマー割り込みなどによって、タスクの実行を任意の時点で強制的に中断するモデル。Goでは、Go 1.14で非同期プリエンプションが完全に導入され、より公平なスケジューリングと低レイテンシのGCを実現しました。このコミットは、その非同期プリエンプションの導入に向けた初期段階の準備作業の一部です。

プリエンプションは、単一のタスクがシステムリソースを独占するのを防ぎ、システムの応答性と公平性を向上させるために不可欠です。

m->locks の役割

m->locks は、GoランタイムのM(OSスレッド)構造体に含まれるフィールドです。このフィールドは、現在のMがプリエンプションを無効にしている回数を追跡するためのカウンターとして機能します。

  • m->locks が0の場合、プリエンプションは有効であり、現在のM上で実行中のゴルーチンはいつでも中断される可能性があります。
  • m->locks が0より大きい場合、プリエンプションは無効化されています。これは、現在のMがスケジューラにとって非常にクリティカルな操作を実行しており、その操作が中断されるとランタイムの整合性が損なわれる可能性があることを示します。

このカウンターは、クリティカルセクションの開始時にインクリメントされ(m->locks++)、終了時にデクリメントされます(m->locks--)。これにより、ネストされたクリティカルセクションでも正しくプリエンプションを制御できます。

技術的詳細

このコミットの核心は、Goランタイムのスケジューラが内部状態を操作している最中にプリエンプションが発生することによる潜在的な問題を回避することです。特に、コメントにある「it can be holding p in a local var」(ローカル変数にPを保持している可能性があるため)という点が重要です。

Goのスケジューラは、M、P、Gという3つのエンティティ間の複雑な関係を管理しています。P(プロセッサ)は、Mがゴルーチンを実行するためのコンテキストを提供し、実行可能なゴルーチンのキューを保持します。スケジューラがPを操作する際、例えばPをMにアタッチしたり、デタッチしたり、Pの内部状態を変更したりすることがあります。

もし、これらの操作中にプリエンプションが発生し、現在のMが中断されて別のMが実行を開始した場合、以下のような問題が発生する可能性があります。

  1. Pの不整合: あるMがPをローカル変数に保持している最中にプリエンプションされ、そのPが別のMによって変更されたり、別のMに割り当てられたりすると、元のMが再開したときに古い、または無効なPの状態を参照してしまう可能性があります。これはデータ競合を引き起こし、スケジューラのクラッシュや不正な動作につながります。
  2. スケジューラのデッドロック: スケジューラが内部ロックを保持している最中にプリエンプションされると、そのロックが解放されないままになり、他のMがそのロックを待ってデッドロックに陥る可能性があります。
  3. ゴルーチンの状態の破損: ゴルーチンの状態(G構造体)を更新している最中にプリエンプションされると、そのゴルーチンが部分的に更新された状態になり、不正な動作を引き起こす可能性があります。

このコミットでm->locksをインクリメント/デクリメントしている関数は、いずれもスケジューラの核心部分に関わるものです。

  • runtime·ready(G *gp): ゴルーチンを実行可能状態にする関数です。この関数は、ゴルーチンをPのローカル実行キューに入れる操作を含みます。この操作中にPの状態が変更されると問題が生じます。
  • runtime·starttheworld(void): ガベージコレクション(GC)などの「ワールドストップ」イベント後に、すべてのMとPを再起動する際に呼び出される関数です。この関数は、スケジューラのグローバルな状態を調整するため、非常にクリティカルなセクションです。
  • runtime·newproc1(...): 新しいゴルーチンを作成する関数です。新しいゴルーチンを初期化し、実行可能キューに入れる操作を含みます。ここでもPのローカルキューへのアクセスや、Gの状態の初期化が行われます。

これらの関数内でm->locks++m->locks--を使用することで、これらのクリティカルな操作が中断されることなくアトミックに実行されることが保証されます。これにより、Goのプリエンプティブスケジューラが導入されても、ランタイムの安定性と整合性が維持されます。

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

変更は src/pkg/runtime/proc.c ファイルに対して行われました。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -274,6 +274,7 @@ void
 runtime·ready(G *gp)
 {
 	// Mark runnable.
+	m->locks++;  // disable preemption because it can be holding p in a local var
 	if(gp->status != Gwaiting) {
 		runtime·printf("goroutine %D has status %d\\n", gp->goid, gp->status);
 		runtime·throw("bad g->status in ready");
@@ -282,6 +283,7 @@ runtime·ready(G *gp)
 	runqput(m->p, gp);
 	if(runtime·atomicload(&runtime·sched.npidle) != 0 && runtime·atomicload(&runtime·sched.nmspinning) == 0)  // TODO: fast atomic
 		wakep();
+	m->locks--;
 }
 
 int32
@@ -398,6 +400,7 @@ runtime·starttheworld(void)
 	G *gp;
 	bool add;
 
+	m->locks++;  // disable preemption because it can be holding p in a local var
 	gp = runtime·netpoll(false);  // non-blocking
 	injectglist(gp);
 	add = needaddgcproc();
@@ -451,6 +454,7 @@ runtime·starttheworld(void)
 		// the maximum number of procs.
 		newm(mhelpgc, nil);
 	}
+	m->locks--;
 }
 
 // Called to start an M.
@@ -1509,6 +1513,7 @@ runtime·newproc1(FuncVal *fn, byte *argp, int32 narg, int32 nret, void *callerp\
 	int32 siz;
 
 //runtime·printf("newproc1 %p %p narg=%d nret=%d\\n", fn->fn, argp, narg, nret);\
+	m->locks++;  // disable preemption because it can be holding p in a local var
 	siz = narg + nret;
 	siz = (siz+7) & ~7;
 
@@ -1555,6 +1560,7 @@ runtime·newproc1(FuncVal *fn, byte *argp, int32 narg, int32 nret, void *callerp\
 
 	if(runtime·atomicload(&runtime·sched.npidle) != 0 && runtime·atomicload(&runtime·sched.nmspinning) == 0 && fn->fn != runtime·main)  // TODO: fast atomic
 		wakep();
+	m->locks--;
 	return newg;
 }
 

コアとなるコードの解説

変更は、以下の3つのGoランタイム関数にm->locks++m->locks--のペアを追加するものです。

  1. runtime·ready(G *gp)

    • 変更内容: 関数の冒頭でm->locks++を、関数の末尾でm->locks--を追加。
    • 解説: この関数は、指定されたゴルーチンgpを実行可能状態にマークし、P(プロセッサ)のローカル実行キュー(runqput(m->p, gp))に入れる役割を担います。この操作は、Pの内部状態を直接変更するため、非常にデリケートです。もしこの処理の途中でプリエンプションが発生し、現在のMが中断された場合、Pの状態が不整合になる可能性があります。例えば、m->pが一時的にローカル変数にキャッシュされている間に、別のMがこのPを奪い取ったり、Pの状態を変更したりすると、データ競合や不正な動作につながります。m->locksをインクリメントすることで、このクリティカルセクション全体でプリエンプションが無効になり、Pの操作がアトミックに完了することが保証されます。
  2. runtime·starttheworld(void)

    • 変更内容: 関数の冒頭でm->locks++を、関数の末尾でm->locks--を追加。
    • 解説: この関数は、Goランタイムが「ワールドストップ」状態(例えば、ガベージコレクションのマークフェーズ中など、すべてのゴルーチンが停止している状態)から「ワールドスタート」状態に移行する際に呼び出されます。このプロセスでは、ネットワークポーラーからのイベント処理(runtime·netpoll)、新しいゴルーチンの注入(injectglist)、GCヘルパーMの起動(newm(mhelpgc, nil))など、スケジューラのグローバルな状態に影響を与える多くの重要な操作が行われます。これらの操作は、M、P、G間の関係を再確立したり、スケジューラのキューを調整したりするため、途中でプリエンプションされると、ランタイム全体の整合性が深刻に損なわれる可能性があります。したがって、この関数全体でプリエンプションを無効にすることは、ランタイムの安定性を確保するために不可欠です。
  3. runtime·newproc1(FuncVal *fn, byte *argp, int32 narg, int32 nret, void *callerp)

    • 変更内容: 関数の冒頭でm->locks++を、関数の末尾でm->locks--を追加。
    • 解説: この関数は、新しいゴルーチンを作成し、そのスタックを初期化し、実行可能状態にしてスケジューラに登録する役割を担います。新しいゴルーチン(newg)の割り当て、スタックのセットアップ、そしてそのゴルーチンをPのローカル実行キューに入れる(最終的にruntime·readyが呼ばれる)など、複数のステップが含まれます。これらのステップの途中でプリエンプションが発生すると、新しく作成中のゴルーチンの状態が不完全なままになり、スケジューラが不正なゴルーチンを参照したり、クラッシュしたりする可能性があります。m->locksによるプリエンプションの無効化は、ゴルーチンの作成と初期化のプロセス全体が中断されることなく、安全に完了することを保証します。

これらの変更は、Goがより高度なプリエンプティブスケジューリングを導入する上で、ランタイムの内部状態を保護し、データ競合や不整合を防ぐための基盤となる修正です。

関連リンク

参考にした情報源リンク

  • Goの公式ドキュメントとリリースノート
  • Goのソースコード (src/pkg/runtime/proc.c)
  • Goスケジューラに関する技術ブログ記事 (上記「関連リンク」に記載)
  • GoのGerritコードレビューシステム (上記「関連リンク」に記載)
  • Goのプリエンプションに関する議論や設計ドキュメント (Web検索を通じて関連情報を参照)

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

このコミットは、Goランタイムのスケジューラ関数の一部において、プリエンプション(横取り)を無効にする変更を導入します。これは、Goのスケジューラが協調的スケジューリングからプリエンプティブ(横取り型)スケジューリングへと進化する過程で必要とされた重要な修正であり、特にスケジューラの内部状態の整合性を保つために行われました。

コミット

commit 4a8ef1f65db072ecd6ff79201338ac75b43640fa
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Jun 3 14:40:38 2013 +0400

    runtime: disable preemption in several scheduler functions
    Required for preemptive scheduler, see the comments for details.
    
    R=golang-dev, khr, iant, khr
    CC=golang-dev
    https://golang.org/cl/9740051

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

https://github.com/golang/go/commit/4a8ef1f65db072ecd6ff79201338ac75b43640fa

元コミット内容

runtime: disable preemption in several scheduler functions Required for preemptive scheduler, see the comments for details.

このコミットは、Goランタイムのいくつかのスケジューラ関数において、プリエンプションを無効にするものです。これはプリエンプティブスケジューラのために必要とされ、詳細はコード内のコメントに記載されています。

変更の背景

Goの初期バージョン(Go 1.0からGo 1.1の一部まで)では、スケジューラは基本的に「協調的(cooperative)」でした。これは、ゴルーチンが自らスケジューラに制御を返す(例えば、チャネル操作、システムコール、明示的なruntime.Gosched()呼び出しなど)まで、CPUを占有し続けることを意味します。この協調的スケジューリングモデルは、シンプルな設計と低いオーバーヘッドという利点がありましたが、いくつかの問題も抱えていました。

主な問題は、CPUバウンドな無限ループや、長時間実行される計算処理を行うゴルーチンが、他のゴルーチンにCPUを明け渡さないために、プログラム全体の応答性を低下させたり、デッドロックを引き起こしたりする可能性があったことです。特に、ガベージコレクション(GC)のようなランタイムの重要な処理が、協調的スケジューリングの制約によって遅延する可能性がありました。

この問題を解決するため、Goチームはより堅牢な「プリエンプティブ(preemptive)」スケジューリングの導入を進めました。プリエンプティブスケジューリングでは、スケジューラが一定時間ごとにゴルーチンの実行を強制的に中断し、別のゴルーチンにCPUを割り当てることができます。これにより、単一のゴルーチンがCPUを独占することを防ぎ、より公平なリソース配分と全体的な応答性の向上を実現します。

しかし、プリエンプティブスケジューリングの導入は、ランタイムの内部、特にスケジューラ自身のコードに複雑性をもたらします。スケジューラが自身の内部状態(例えば、M, P, G構造体間のリンクや、Pのローカル変数への保持など)を操作している最中にプリエンプションが発生すると、データ競合や不整合が生じる可能性があります。このコミットは、まさにそのようなクリティカルセクションにおいて、一時的にプリエンプションを無効にすることで、スケジューラの整合性を保護するために導入されました。

前提知識の解説

Goランタイムスケジューラ (M, P, Gモデル)

Goのスケジューラは、M(Machine/Thread)、P(Processor/Context)、G(Goroutine)という3つの主要な抽象化に基づいて設計されています。

  • G (Goroutine): Goにおける軽量な実行単位です。Goプログラム内で並行して実行される関数やメソッドのインスタンスを表します。各Gは独自のスタックを持ち、スケジューラによってM上で実行されます。
  • M (Machine/Thread): オペレーティングシステム(OS)のスレッドを表します。Goランタイムは、OSスレッドを生成し、その上でGoのコードを実行します。MはGを実行するための物理的なリソースを提供します。
  • P (Processor/Context): MとGを仲介する論理的なプロセッサです。Pは、実行可能なGのキュー(ローカル実行キュー)を保持し、MがGを実行するためのコンテキストを提供します。MはPにアタッチされ、PのローカルキューからGを取得して実行します。これにより、複数のMが同時にGを実行する際に、Gのスケジューリングを効率的に調整します。Pの数は、通常、CPUコアの数に等しく設定され、GOMAXPROCS環境変数で制御できます。

スケジューラの基本的な流れは、MがPにアタッチされ、PのローカルキューからGを取り出して実行します。Gがブロックされる(例: ネットワークI/O待ち)と、MはPを解放し、別のMがそのPにアタッチされて別のGを実行できます。

プリエンプション (Preemption) の概念

プリエンプションとは、実行中のタスク(この場合はゴルーチン)が、自ら制御を放棄することなく、外部のメカニズム(スケジューラ)によって強制的に中断され、別のタスクにCPUの実行権が移されることです。

  • 協調的プリエンプション: タスクが特定のポイント(関数呼び出し、システムコールなど)で自発的にスケジューラに制御を返すことを期待するモデル。Goの初期バージョンでは、関数プロローグでのスタックチェックを利用して、長時間実行されるループでもスケジューラに制御を返す機会を設けていました。
  • 非同期プリエンプション(Async Preemption): 外部からのシグナルやタイマー割り込みなどによって、タスクの実行を任意の時点で強制的に中断するモデル。Goでは、Go 1.14で非同期プリエンプションが完全に導入され、より公平なスケジューリングと低レイテンシのGCを実現しました。このコミットは、その非同期プリエンプションの導入に向けた初期段階の準備作業の一部です。

プリエンプションは、単一のタスクがシステムリソースを独占するのを防ぎ、システムの応答性と公平性を向上させるために不可欠です。

m->locks の役割

m->locks は、GoランタイムのM(OSスレッド)構造体に含まれるフィールドです。このフィールドは、現在のMがプリエンプションを無効にしている回数を追跡するためのカウンターとして機能します。

  • m->locks が0の場合、プリエンプションは有効であり、現在のM上で実行中のゴルーチンはいつでも中断される可能性があります。
  • m->locks が0より大きい場合、プリエンプションは無効化されています。これは、現在のMがスケジューラにとって非常にクリティカルな操作を実行しており、その操作が中断されるとランタイムの整合性が損なわれる可能性があることを示します。

このカウンターは、クリティカルセクションの開始時にインクリメントされ(m->locks++)、終了時にデクリメントされます(m->locks--)。これにより、ネストされたクリティカルセクションでも正しくプリエンプションを制御できます。

技術的詳細

このコミットの核心は、Goランタイムのスケジューラが内部状態を操作している最中にプリエンプションが発生することによる潜在的な問題を回避することです。特に、コメントにある「it can be holding p in a local var」(ローカル変数にPを保持している可能性があるため)という点が重要です。

Goのスケジューラは、M、P、Gという3つのエンティティ間の複雑な関係を管理しています。P(プロセッサ)は、Mがゴルーチンを実行するためのコンテキストを提供し、実行可能なゴルーチンのキューを保持します。スケジューラがPを操作する際、例えばPをMにアタッチしたり、デタッチしたり、Pの内部状態を変更したりすることがあります。

もし、これらの操作中にプリエンプションが発生し、現在のMが中断されて別のMが実行を開始した場合、以下のような問題が発生する可能性があります。

  1. Pの不整合: あるMがPをローカル変数に保持している最中にプリエンプションされ、そのPが別のMによって変更されたり、別のMに割り当てられたりすると、元のMが再開したときに古い、または無効なPの状態を参照してしまう可能性があります。これはデータ競合を引き起こし、スケジューラのクラッシュや不正な動作につながります。
  2. スケジューラのデッドロック: スケジューラが内部ロックを保持している最中にプリエンプションされると、そのロックが解放されないままになり、他のMがそのロックを待ってデッドロックに陥る可能性があります。
  3. ゴルーチンの状態の破損: ゴルーチンの状態(G構造体)を更新している最中にプリエンプションされると、そのゴルーチンが部分的に更新された状態になり、不正な動作を引き起こす可能性があります。

このコミットでm->locksをインクリメント/デクリメントしている関数は、いずれもスケジューラの核心部分に関わるものです。

  • runtime·ready(G *gp): ゴルーチンを実行可能状態にする関数です。この関数は、ゴルーチンをPのローカル実行キューに入れる操作を含みます。この操作中にPの状態が変更されると問題が生じます。
  • runtime·starttheworld(void): ガベージコレクション(GC)などの「ワールドストップ」イベント後に、すべてのMとPを再起動する際に呼び出される関数です。この関数は、スケジューラのグローバルな状態を調整するため、非常にクリティカルなセクションです。
  • runtime·newproc1(...): 新しいゴルーチンを作成する関数です。新しいゴルーチンを初期化し、実行可能キューに入れる操作を含みます。ここでもPのローカルキューへのアクセスや、Gの状態の初期化が行われます。

これらの関数内でm->locks++m->locks--を使用することで、これらのクリティカルな操作が中断されることなくアトミックに実行されることが保証されます。これにより、Goのプリエンプティブスケジューラが導入されても、ランタイムの安定性と整合性が維持されます。

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

変更は src/pkg/runtime/proc.c ファイルに対して行われました。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -274,6 +274,7 @@ void
 runtime·ready(G *gp)
 {
 	// Mark runnable.
+	m->locks++;  // disable preemption because it can be holding p in a local var
 	if(gp->status != Gwaiting) {
 		runtime·printf("goroutine %D has status %d\\n", gp->goid, gp->status);
 		runtime·throw("bad g->status in ready");
@@ -282,6 +283,7 @@ runtime·ready(G *gp)
 	runqput(m->p, gp);
 	if(runtime·atomicload(&runtime·sched.npidle) != 0 && runtime·atomicload(&runtime·sched.nmspinning) == 0)  // TODO: fast atomic
 		wakep();
+	m->locks--;
 }
 
 int32
@@ -398,6 +400,7 @@ runtime·starttheworld(void)
 	G *gp;
 	bool add;
 
+	m->locks++;  // disable preemption because it can be holding p in a local var
 	gp = runtime·netpoll(false);  // non-blocking
 	injectglist(gp);
 	add = needaddgcproc();
@@ -451,6 +454,7 @@ runtime·starttheworld(void)
 		// the maximum number of procs.
 		newm(mhelpgc, nil);
 	}
+	m->locks--;
 }
 
 // Called to start an M.
@@ -1509,6 +1513,7 @@ runtime·newproc1(FuncVal *fn, byte *argp, int32 narg, int32 nret, void *callerp\
 	int32 siz;
 
 //runtime·printf("newproc1 %p %p narg=%d nret=%d\\n", fn->fn, argp, narg, nret);\
+	m->locks++;  // disable preemption because it can be holding p in a local var
 	siz = narg + nret;
 	siz = (siz+7) & ~7;
 
@@ -1555,6 +1560,7 @@ runtime·newproc1(FuncVal *fn, byte *argp, int32 narg, int32 nret, void *callerp\
 
 	if(runtime·atomicload(&runtime·sched.npidle) != 0 && runtime·atomicload(&runtime·sched.nmspinning) == 0 && fn->fn != runtime·main)  // TODO: fast atomic
 		wakep();
+	m->locks--;
 	return newg;
 }
 

コアとなるコードの解説

変更は、以下の3つのGoランタイム関数にm->locks++m->locks--のペアを追加するものです。

  1. runtime·ready(G *gp)

    • 変更内容: 関数の冒頭でm->locks++を、関数の末尾でm->locks--を追加。
    • 解説: この関数は、指定されたゴルーチンgpを実行可能状態にマークし、P(プロセッサ)のローカル実行キュー(runqput(m->p, gp))に入れる役割を担います。この操作は、Pの内部状態を直接変更するため、非常にデリケートです。もしこの処理の途中でプリエンプションが発生し、現在のMが中断された場合、Pの状態が不整合になる可能性があります。例えば、m->pが一時的にローカル変数にキャッシュされている間に、別のMがこのPを奪い取ったり、Pの状態を変更したりすると、データ競合や不正な動作につながります。m->locksをインクリメントすることで、このクリティカルセクション全体でプリエンプションが無効になり、Pの操作がアトミックに完了することが保証されます。
  2. runtime·starttheworld(void)

    • 変更内容: 関数の冒頭でm->locks++を、関数の末尾でm->locks--を追加。
    • 解説: この関数は、Goランタイムが「ワールドストップ」状態(例えば、ガベージコレクションのマークフェーズ中など、すべてのゴルーチンが停止している状態)から「ワールドスタート」状態に移行する際に呼び出されます。このプロセスでは、ネットワークポーラーからのイベント処理(runtime·netpoll)、新しいゴルーチンの注入(injectglist)、GCヘルパーMの起動(newm(mhelpgc, nil))など、スケジューラのグローバルな状態に影響を与える多くの重要な操作が行われます。これらの操作は、M、P、G間の関係を再確立したり、スケジューラのキューを調整したりするため、途中でプリエンプションされると、ランタイム全体の整合性が深刻に損なわれる可能性があります。したがって、この関数全体でプリエンプションを無効にすることは、ランタイムの安定性を確保するために不可欠です。
  3. runtime·newproc1(FuncVal *fn, byte *argp, int32 narg, int32 nret, void *callerp)

    • 変更内容: 関数の冒頭でm->locks++を、関数の末尾でm->locks--を追加。
    • 解説: この関数は、新しいゴルーチンを作成し、そのスタックを初期化し、実行可能状態にしてスケジューラに登録する役割を担います。新しいゴルーチン(newg)の割り当て、スタックのセットアップ、そしてそのゴルーチンをPのローカル実行キューに入れる(最終的にruntime·readyが呼ばれる)など、複数のステップが含まれます。これらのステップの途中でプリエンプションが発生すると、新しく作成中のゴルーチンの状態が不完全なままになり、スケジューラが不正なゴルーチンを参照したり、クラッシュしたりする可能性があります。m->locksによるプリエンプションの無効化は、ゴルーチンの作成と初期化のプロセス全体が中断されることなく、安全に完了することを保証します。

これらの変更は、Goがより高度なプリエンプティブスケジューリングを導入する上で、ランタイムの内部状態を保護し、データ競合や不整合を防ぐための基盤となる修正です。

関連リンク

参考にした情報源リンク

  • Goの公式ドキュメントとリリースノート
  • Goのソースコード (src/pkg/runtime/proc.c)
  • Goスケジューラに関する技術ブログ記事 (上記「関連リンク」に記載)
  • GoのGerritコードレビューシステム (上記「関連リンク」に記載)
  • Goのプリエンプションに関する議論や設計ドキュメント (Web検索を通じて関連情報を参照)