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

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

このコミットは、GoランタイムにおけるMCPU(Machine CPU)のカウント方法に関する修正です。具体的には、プログラム起動時に生成される「ブートストラップM」(OSスレッド)がMCPUの会計に適切に含まれるように変更されています。これにより、Goスケジューラが管理するOSスレッドの数がより正確に反映されるようになります。

コミット

commit 2572ca2ff2cda56a3fb1732a4b628fc7b85ea798
Author: Hector Chu <hectorchu@gmail.com>
Date:   Tue Oct 25 08:35:20 2011 +0100

    runtime: include bootstrap m in mcpu accounting
    
    R=rsc, dvyukov
    CC=golang-dev
    https://golang.org/cl/5307052

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

https://github.com/golang/go/commit/2572ca2ff2cda56a3fb1732a4b628fc7b85ea798

元コミット内容

runtime: include bootstrap m in mcpu accounting

変更の背景

Goランタイムは、Goroutine、M(Machine/OSスレッド)、P(Processor/論理プロセッサ)という3つの主要なエンティティからなるスケジューラモデルを採用しています。MはOSスレッドを表し、実際にCPU上でコードを実行する役割を担います。MCPUは、Goランタイムが現在アクティブに使用しているOSスレッドの数を追跡するための内部カウンタです。

このコミット以前は、Goプログラムが起動する際に最初に生成されるOSスレッド(「ブートストラップM」と呼ばれる)が、MCPUの会計に適切に含まれていませんでした。これは、MCPUのカウントが、Goスケジューラによって明示的に生成または管理されるMのみを対象としていたためと考えられます。

ブートストラップMがMCPUのカウントに含まれない場合、Goランタイムが認識しているアクティブなOSスレッドの総数が実際よりも少なくなり、スケジューラの判断に影響を与える可能性がありました。特に、GOMAXPROCS(同時に実行可能なOSスレッドの最大数)の制約下で、スケジューラが新しいMを起動するかどうかを決定する際に、誤った情報に基づいて判断を下すリスクがありました。

このコミットは、この不整合を修正し、ブートストラップMもMCPUの会計に含めることで、GoランタイムがOSスレッドの利用状況をより正確に把握できるようにすることを目的としています。これにより、スケジューラの動作がより堅牢になり、リソース管理が最適化されます。

前提知識の解説

Goランタイムスケジューラ (GMPモデル)

Goランタイムのスケジューラは、Goroutine (G)、M (Machine)、P (Processor) の3つの要素から構成されるGMPモデルを採用しています。

  • G (Goroutine): Goにおける軽量な並行実行単位です。OSスレッドよりもはるかに軽量で、数百万個のGoroutineを同時に実行できます。
  • M (Machine): OSスレッドを表します。Goランタイムは、必要に応じてOSスレッドを生成し、GoroutineをM上で実行します。MはOSのスケジューラによって管理されます。
  • P (Processor): 論理プロセッサを表します。PはMとGoroutineの間の仲介役となり、MがGoroutineを実行するためのコンテキストを提供します。GOMAXPROCS環境変数は、同時に実行可能なPの最大数を制御します。

Goスケジューラは、GをPにディスパッチし、PはMにアタッチされてGを実行します。Mがシステムコールなどでブロックされると、Pは別のMにアタッチされ、他のGoroutineの実行を継続できます。

MCPU会計 (mcpu accounting)

MCPUは、Goランタイムが現在アクティブに利用しているOSスレッド(M)の数を追跡するための内部カウンタです。このカウンタは、GOMAXPROCSによって設定された最大値を超えないように、新しいMの生成や既存のMの停止を制御するために使用されます。

MCPUの正確な会計は、Goランタイムがシステムリソースを効率的に利用し、過剰なOSスレッドの生成を防ぐ上で非常に重要です。

ブートストラップM (bootstrap m)

Goプログラムが起動すると、OSによって最初に1つのスレッドが生成され、そのスレッド上でGoランタイムの初期化処理が開始されます。この初期スレッドが「ブートストラップM」です。Goランタイムのスケジューラが本格的に動作を開始する前に存在し、ランタイムの初期設定やGoroutineの起動など、基盤となる処理を実行します。

技術的詳細

このコミットは、src/pkg/runtime/proc.c ファイル内の runtime·schedinit 関数と schedule 関数に修正を加えています。

runtime·schedinit 関数

runtime·schedinit は、Goランタイムのスケジューラが初期化される際に呼び出される関数です。この関数内で、ブートストラップMがMCPUの会計に含まれるように変更が加えられています。

変更前は、ブートストラップMはMCPUのカウントに含まれていませんでした。変更後は、canaddmcpu() の呼び出しと m->helpgc = 1 の設定により、ブートストラップMがMCPUの会計に組み込まれるようになります。

  • canaddmcpu(): この関数は、MCPUカウンタをインクリメントする役割を担います。ブートストラップMの起動時にこれを呼び出すことで、MCPUのカウントにブートストラップMが追加されます。
  • m->helpgc = 1: このフラグは、現在のM(ブートストラップM)がスケジューラによってMCPUのデクリメント処理を行う必要があることを示します。これは、schedule() 関数内で処理されます。
  • runtime·sched.grunning++: これは、実行中のGoroutineの数をインクリメントしています。ブートストラップMは初期のGoroutineを実行しているため、このカウントも適切に調整されます。

schedule 関数

schedule 関数は、Goスケジューラの中心的なループであり、Goroutineの実行を管理します。この関数内で、m->helpgc フラグが設定されているM(ブートストラップMまたは starttheworld によって起動された新しいM)に対して、MCPUのデクリメント処理が行われるように修正されています。

変更前は、ブートストラップMがMCPUの会計に含まれていなかったため、その終了時にMCPUをデクリメントするロジックは存在しませんでした。変更後は、m->helpgctrue の場合に、runtime·xadd(&runtime·sched.atomic, -1<<mcpuShift) を呼び出してMCPUカウンタをデクリメントするようになります。これにより、ブートストラップMがスケジューラから離れる際に、MCPUのカウントが正確に減少します。

また、else if(m->nextg != nil) の後に else { runtime·throw("invalid m state in scheduler"); } が追加されており、スケジューラにおけるMの不正な状態を検出した場合にパニックを発生させることで、ランタイムの堅牢性が向上しています。

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

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -128,6 +128,8 @@ Sched runtime·sched;
 int32 runtime·gomaxprocs;
 bool runtime·singleproc;
 
+static bool canaddmcpu(void);\n
+
 // An m that is waiting for notewakeup(&m->havenextg).  This may
 // only be accessed while the scheduler lock is held.  This is used to
 // minimize the number of times we call notewakeup while the scheduler
@@ -202,6 +204,10 @@ runtime·schedinit(void)
 	setmcpumax(runtime·gomaxprocs);
 	runtime·singleproc = runtime·gomaxprocs == 1;
 
+	canaddmcpu();	// mcpu++ to account for bootstrap m
+	m->helpgc = 1;	// flag to tell schedule() to mcpu--
+	runtime·sched.grunning++;
+
 	mstats.enablegc = 1;
 	m->nomemprof--;
 }
@@ -811,6 +817,7 @@ schedule(G *gp)
 			readylocked(gp);
 		}
 	} else if(m->helpgc) {
+\t\t// Bootstrap m or new m started by starttheworld.
 \t\t// atomic { mcpu-- }\n
 \t\tv = runtime·xadd(&runtime·sched.atomic, -1<<mcpuShift);\n
 \t\tif(atomic_mcpu(v) > maxgomaxprocs)\n
@@ -818,6 +825,10 @@ schedule(G *gp)
 \t\t// Compensate for increment in starttheworld().\n
 \t\truntime·sched.grunning--;\n
 \t\tm->helpgc = 0;\n
+\t} else if(m->nextg != nil) {\n
+\t\t// New m started by matchmg.\n
+\t} else {\n
+\t\truntime·throw(\"invalid m state in scheduler\");\n
 \t}\n
 
 \t// Find (or wait for) g to run.  Unlocks runtime·sched.\n

コアとなるコードの解説

src/pkg/runtime/proc.c

  • static bool canaddmcpu(void); の追加: canaddmcpu 関数の前方宣言が追加されました。この関数は、MCPUカウンタをインクリメントする役割を担います。

  • runtime·schedinit 関数内の変更:

    	canaddmcpu();	// mcpu++ to account for bootstrap m
    	m->helpgc = 1;	// flag to tell schedule() to mcpu--
    	runtime·sched.grunning++;
    

    runtime·schedinit の中で、canaddmcpu() が呼び出され、ブートストラップMがMCPUの会計に含められるようになりました。m->helpgc = 1 は、このMがスケジューラによってMCPUのデクリメント処理を必要とすることを示すフラグです。runtime·sched.grunning++ は、実行中のGoroutineの数をインクリメントし、ブートストラップMが初期のGoroutineを実行していることを反映します。

  • schedule 関数内の変更:

    	} else if(m->helpgc) {
    		// Bootstrap m or new m started by starttheworld.
    		// atomic { mcpu-- }
    		v = runtime·xadd(&runtime·sched.atomic, -1<<mcpuShift);
    		if(atomic_mcpu(v) > maxgomaxprocs)
    			runtime·throw("mcpu accounting error");
    		// Compensate for increment in starttheworld().
    		runtime·sched.grunning--;
    		m->helpgc = 0;
    	} else if(m->nextg != nil) {
    		// New m started by matchmg.
    	} else {
    		runtime·throw("invalid m state in scheduler");
    	}
    

    schedule 関数内で、m->helpgctrue の場合に、MCPUカウンタをデクリメントするロジックが追加されました。これは、ブートストラップMがスケジューラから離れる際に、MCPUのカウントを正確に減少させるためのものです。 また、else if(m->nextg != nil) の後に else { runtime·throw("invalid m state in scheduler"); } が追加され、スケジューラにおけるMの不正な状態を検出した場合にパニックを発生させることで、ランタイムの堅牢性が向上しています。

関連リンク

参考にした情報源リンク

  • Goのスケジューラに関する公式ドキュメントやブログ記事(一般的なGMPモデルの解説)
  • Goのソースコード(src/pkg/runtime/proc.c の関連部分)
  • Goのコミット履歴と関連する議論(Go CLのコメントなど)
  • MCPU会計に関するGoランタイムの内部実装に関する情報
  • GOMAXPROCS 環境変数に関するGoのドキュメント
  • runtime·xadd などのアトミック操作に関する情報
  • runtime·throw に関する情報
  • starttheworldmatchmg など、Goランタイムの内部関数に関する情報
    • これらの関数は、Goランタイムの内部的な動作に関連しており、通常は直接ユーザーが呼び出すものではありません。Goのソースコードを深く掘り下げることで理解できます。
  • Goのガベージコレクション(GC)とスケジューラの連携に関する情報(helpgc フラグの文脈で)
    • helpgc フラグは、GCのヘルパーMがスケジューラによってMCPUのデクリメント処理を行う必要があることを示唆しています。これは、GCがMを一時的に利用し、その後解放する際のMCPU会計の調整に関連します。
    • Go's garbage collector
    • Go's runtime: a deep dive into the scheduler

これらの情報源は、Goランタイムの内部動作、特にスケジューラとMCPU会計のメカニズムを理解する上で役立ちます。