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

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

このコミットは、Go言語のランタイムにおける重要な変更を含んでいます。特に、GOMAXPROCS環境変数の意味合いが「使用するCPUの数」に変更され、「スレッドの数」ではなくなりました。これにより、GoのスケジューラがどのようにゴルーチンをOSスレッドにマッピングし、CPUリソースを管理するかの根本的なアプローチが変更されています。

また、Darwin (macOS) における syscall.Syscall6 のバグ修正、chanclient のバグ修正、ネットワークテストからの $GOMAXPROCS の削除、そしてランタイムにデバッグ用の printf および sys.printhex 関数が追加されています。

コミット

commit efc86a74e4e1f0bf38e42271dae11d7a23026b4d
Author: Russ Cox <rsc@golang.org>
Date:   Tue Nov 25 16:48:10 2008 -0800

    change meaning of $GOMAXPROCS to number of cpus to use,
    not number of threads.  can still starve all the other threads,
    but only by looping, not by waiting in a system call.
    
    fix darwin syscall.Syscall6 bug.
    
    fix chanclient bug.
    
    delete $GOMAXPROCS from network tests.
    
    add stripped down printf, sys.printhex to runtime.
    
    R=r
    DELTA=355  (217 added, 36 deleted, 102 changed)
    OCL=20017
    CL=20019

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

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

元コミット内容

change meaning of $GOMAXPROCS to number of cpus to use,
not number of threads.  can still starve all the other threads,
but only by looping, not by waiting in a system call.

fix darwin syscall.Syscall6 bug.

fix chanclient bug.

delete $GOMAXPROCS from network tests.

add stripped down printf, sys.printhex to runtime.

R=r
DELTA=355  (217 added, 36 deleted, 102 changed)
OCL=20017
CL=20019

変更の背景

このコミットが行われた2008年後半は、Go言語がまだ初期開発段階にあり、ランタイムとスケジューラの設計が活発に行われていた時期です。初期のGoランタイムは、ゴルーチンをOSスレッドにマッピングする際に、GOMAXPROCSをOSスレッドの最大数として解釈していました。しかし、このモデルでは、あるゴルーチンがシステムコール(例:ファイルI/O、ネットワーク通信)でブロックされると、そのゴルーチンが実行されているOSスレッドもブロックされ、他の実行可能なゴルーチンがCPUリソースを利用できなくなるという問題がありました。これは「スレッドの飢餓」を引き起こし、並行処理の効率を低下させる要因となります。

このコミットの背景には、Goの並行処理モデルの核となる「ゴルーチン」の効率的なスケジューリングを実現するための、より洗練されたアプローチへの移行があります。特に、システムコールによるブロッキングが全体のパフォーマンスに与える影響を最小限に抑えることが喫緊の課題でした。GOMAXPROCSの意味を「使用するCPUの数」に変更することで、Goランタイムは、システムコールでブロックされたOSスレッドから、他の実行可能なゴルーチンを別のOSスレッドに切り替える(または新しいOSスレッドを起動する)メカニズムを導入し、CPUの利用効率を最大化しようとしています。

前提知識の解説

Goの並行処理モデル(初期の概念)

Go言語の最大の特徴の一つは、軽量な並行処理単位である「ゴルーチン (goroutine)」と、それらを安全に通信させるための「チャネル (channel)」です。

  • ゴルーチン: OSスレッドよりもはるかに軽量な実行単位です。数千、数万のゴルーチンを同時に起動しても、OSスレッドのようにリソースを大量に消費することはありません。Goランタイムがこれらのゴルーチンを少数のOSスレッド(M:Nスケジューリングモデルの「M」)にマッピングして実行します。
  • Goスケジューラ: Goランタイム内部に存在するスケジューラは、ゴルーチンをOSスレッドに割り当て、実行を管理します。初期のGoスケジューラは、OSスレッドの数をGOMAXPROCSで制御していましたが、システムコールによるブロッキングが課題でした。

GOMAXPROCS環境変数

GOMAXPROCSは、Goプログラムが同時に実行できるOSスレッドの最大数を制御するための環境変数です。

  • 変更前: このコミット以前は、GOMAXPROCSはGoランタイムが利用するOSスレッドの最大数を直接指定していました。例えば、GOMAXPROCS=2と設定すると、Goランタイムは最大2つのOSスレッドを使用してゴルーチンを実行します。このモデルでは、もし2つのOSスレッドが両方ともシステムコールでブロックされると、他の実行可能なゴルーチンがあってもCPUがアイドル状態になる可能性がありました。
  • 変更後: このコミットにより、GOMAXPROCSは「同時に実行可能なCPUの論理プロセッサ数」を意味するようになりました。これは、Goランタイムが同時に実行できるゴルーチンの数を制御するものであり、OSスレッドの数とは直接的に結びつかなくなります。Goランタイムは、GOMAXPROCSで指定された数のCPUコアを最大限に活用しようとします。システムコールでブロックされたゴルーチンは、そのOSスレッドを解放し、他のゴルーチンが別のOSスレッドで実行を継続できるようにします。

システムコールとブロッキング

  • システムコール: プログラムがOSの機能(ファイルI/O、ネットワーク通信、メモリ管理など)を利用するために、OSカーネルに要求を出すことです。システムコールは、カーネルモードでの実行を伴い、完了するまで時間がかかる場合があります。
  • ブロッキングシステムコール: システムコールの中には、処理が完了するまでプログラムの実行を一時停止させるものがあります(例:readがデータを待つ場合)。これをブロッキングシステムコールと呼びます。

M:Nスケジューリングモデル(GoのP, M, Gモデルの萌芽)

Goのスケジューラは、M:Nスケジューリングモデルを採用しています。これは、M個のゴルーチンをN個のOSスレッドにマッピングするものです。

  • G (Goroutine): Goの軽量な実行単位。
  • M (Machine/OS Thread): OSスレッド。GoランタイムがOSから取得し、ゴルーチンを実行する実際のOSスレッド。
  • P (Processor/Logical Processor): 論理プロセッサ。GOMAXPROCSで指定される数に相当し、MがGを実行するためのコンテキストを提供します。Pは、実行可能なGのキューを保持し、MにGを割り当てます。

このコミットは、Pの概念が明確になる前の段階で、Mがシステムコールでブロックされた際に、他のMがGを実行できるようにするメカニズムを導入しようとしています。

技術的詳細

このコミットの主要な技術的変更点は以下の通りです。

  1. GOMAXPROCSのセマンティクス変更:

    • src/runtime/proc.c内のSched構造体に、mmax(最大スレッド数)からmcpumax(最大CPU数)への変更が見られます。
    • sched.mcpu(現在CPUで実行中のMの数)とsched.msyscall(システムコール中のMの数)という新しいフィールドが追加され、GoランタイムがCPU利用状況とシステムコールによるブロッキング状況をより詳細に追跡できるようになりました。
    • schedinit関数でGOMAXPROCS環境変数を読み込み、sched.mcpumaxに設定するようになりました。
  2. システムコール時のスケジューラ連携:

    • src/lib/syscall/asm_amd64_darwin.ssrc/lib/syscall/asm_amd64_linux.sにおいて、syscall.Syscallおよびsyscall.Syscall6のラッパーにsys·entersyscallsys·exitsyscallの呼び出しが追加されました。
    • sys·entersyscallが呼び出されると、現在のMがシステムコールに入ったことをランタイムに通知し、sched.mcpuをデクリメントし、sched.msyscallをインクリメントします。これにより、このMがCPUを占有していないことをスケジューラに伝えます。もし実行可能なゴルーチンが待機している場合、matchmg関数が呼び出され、新しいMが起動されるか、既存のMがゴルーチンを実行するように促されます。
    • sys·exitsyscallが呼び出されると、システムコールから戻ったことをランタイムに通知し、sched.msyscallをデクリメントし、sched.mcpuをインクリメントします。これにより、このMが再びCPUを利用可能になったことをスケジューラに伝えます。もしsched.mcpusched.mcpumaxを超えている場合(つまり、利用可能なCPU数を超えてMが実行可能になった場合)、sys·gosched()が呼び出され、現在のMはゴルーチンを解放してスリープ状態に入る可能性があります。
  3. スケジューラの改善:

    • src/runtime/proc.cready関数とnextgandunlock関数が変更され、sched.mcpusched.mcpumaxを考慮してゴルーチンをスケジューリングするようになりました。
    • matchmgという新しい関数が導入されました。この関数は、sched.mcpusched.mcpumaxよりも小さい場合に、待機中のゴルーチン(G)があれば、新しいOSスレッド(M)を起動するか、既存の待機中のMにGを割り当てて起動します。これにより、CPUリソースが最大限に活用されるようになります。
    • scheduler関数も、ゴルーチンの実行が完了した際にsched.mcpuをデクリメントするロジックが追加されました。
  4. デバッグ用出力関数の追加:

    • src/runtime/print.cに、簡易版のprintf関数とsys.printhex関数が追加されました。これらはランタイム内部のデバッグ出力に使用されます。
    • src/runtime/rt1_amd64_darwin.csrc/runtime/rt1_amd64_linux.cでは、レジスタやアドレスの出力にsys.printpointerの代わりにsys.printhexを使用するように変更されました。
  5. テストコードの変更:

    • test/dialgoogle.gotest/tcpserver.goから、テスト実行時のGOMAXPROCSの設定が削除されました。これは、GOMAXPROCSのセマンティクス変更に伴い、テストが特定のOSスレッド数に依存する必要がなくなったためです。

これらの変更は、Goのスケジューラがシステムコールによるブロッキングを透過的に処理し、CPUリソースをより効率的に利用するための基盤を築くものであり、Goの並行処理モデルの進化における重要な一歩と言えます。

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

このコミットのコアとなる変更は、主にsrc/runtime/proc.cと、システムコールをラップするアセンブリファイル(src/lib/syscall/asm_amd64_darwin.ssrc/lib/syscall/asm_amd64_linux.s)に集中しています。

src/runtime/proc.c

  • Sched構造体の変更:

    --- a/src/runtime/proc.c
    +++ b/src/runtime/proc.c
    @@ -49,8 +50,10 @@ struct Sched {
     
     	M *mhead;	// ms waiting for work
     	int32 mwait;	// number of ms waiting for work
    -	int32 mcount;	// number of ms that are alive
    -	int32 mmax;	// max number of ms allowed
    +	int32 mcount;	// number of ms that have been created
    +	int32 mcpu;	// number of ms executing on cpu
    +	int32 mcpumax;	// max number of ms allowed on cpu
    +	int32 msyscall;	// number of ms in system calls
     
     	int32 predawn;	// running initialization, don't run new gs.
     };
    

    mmaxmcpumaxに変わり、mcpumsyscallが追加されました。

  • schedinit関数でのGOMAXPROCSの解釈変更:

    --- a/src/runtime/proc.c
    +++ b/src/runtime/proc.c
    @@ -88,10 +91,10 @@ schedinit(void)
      	int32 n;
      	byte *p;
      
    -	sched.mmax = 1;
    +	sched.mcpumax = 1;
      	p = getenv("GOMAXPROCS");
      	if(p != nil && (n = atoi(p)) != 0)
    -		sched.mmax = n;
    +		sched.mcpumax = n;
      	sched.mcount = 1;
      	sched.predawn = 1;
      }
    
  • ready関数でのmatchmgの呼び出し:

    --- a/src/runtime/proc.c
    +++ b/src/runtime/proc.c
    @@ -310,42 +302,49 @@ readylocked(G *g)
      		throw("bad g->status in ready");
      	g->status = Grunnable;
      
    -	// Before we've gotten to main·main,
    -	// only queue new gs, don't run them
    -	// or try to allocate new ms for them.
    -	// That includes main·main itself.
    -	if(sched.predawn){
    -		gput(g);
    -	}
    -
    -	// Else if there's an m waiting, give it g.
    -	else if((m = mget()) != nil){
    -		m->nextg = g;
    -		notewakeup(&m->havenextg);
    -	}
    -
    -	// Else put g on queue, kicking off new m if needed.
    -	else{
    -		gput(g);
    -		if(sched.mcount < sched.mmax)
    -			mnew();
    -	}
    +	gput(g);
    +	if(!sched.predawn)
    +		matchmg();
      }
    

    mnew()の代わりにmatchmg()が呼ばれるようになりました。

  • nextgandunlock関数でのスケジューリングロジックの変更:

    --- a/src/runtime/proc.c
    +++ b/src/runtime/proc.c
    @@ -319,10 +319,20 @@ nextgandunlock(void)
      	G *gp;
      
    -	if((gp = gget()) != nil){
    +	// On startup, each m is assigned a nextg and
    +	// has already been accounted for in mcpu.
    +	if(m->nextg != nil) {
    +		gp = m->nextg;
    +		m->nextg = nil;
      		unlock(&sched);
    +		if(debug > 1) {
    +			lock(&debuglock);
    +			printf("m%d nextg found g%d\n", m->id, gp->goid);
    +			unlock(&debuglock);
    +		}
      		return gp;
      	}
      
    +	// Otherwise, look for work.
    +	if(sched.mcpu < sched.mcpumax && (gp=gget()) != nil) {
    +		sched.mcpu++;
    +		unlock(&sched);
    +		if(debug > 1) {
    +			lock(&debuglock);
    +			printf("m%d nextg got g%d\n", m->id, gp->goid);
    +			unlock(&debuglock);
    +		}
    +		return gp;
    +	}
    +
    +	// Otherwise, sleep.
      	mput(m);
    -	if(sched.mcount == sched.mwait)
    +	if(sched.mcpu == 0 && sched.msyscall == 0)
      		throw("all goroutines are asleep - deadlock!");
      	m->nextg = nil;
      	noteclear(&m->havenextg);
    

    sched.mcpusched.mcpumaxを考慮したゴルーチン取得ロジックが追加されました。

  • matchmg関数の新規追加:

    --- a/src/runtime/proc.c
    +++ b/src/runtime/proc.c
    @@ -366,6 +370,47 @@ mstart(void)
      	scheduler();
      }
      
    +// Kick of new ms as needed (up to mcpumax).
    +// There are already `other' other cpus that will
    +// start looking for goroutines shortly.
    +// Sched is locked.
    +static void
    +matchmg(void)
    +{
    +	M *m;
    +	G *g;
    +
    +	if(debug > 1 && sched.ghead != nil) {
    +		lock(&debuglock);
    +		printf("matchmg mcpu=%d mcpumax=%d gwait=%d\n", sched.mcpu, sched.mcpumax, sched.gwait);
    +		unlock(&debuglock);
    +	}
    +
    +	while(sched.mcpu < sched.mcpumax && (g = gget()) != nil){
    +		sched.mcpu++;
    +		if((m = mget()) != nil){
    +			if(debug > 1) {
    +				lock(&debuglock);
    +				printf("wakeup m%d g%d\n", m->id, g->goid);
    +				unlock(&debuglock);
    +			}
    +			m->nextg = g;
    +			notewakeup(&m->havenextg);
    +		}else{
    +			m = mal(sizeof(M));
    +			m->g0 = malg(1024);
    +			m->nextg = g;
    +			m->id = sched.mcount++;
    +			if(debug) {
    +				lock(&debuglock);
    +				printf("alloc m%d g%d\n", m->id, g->goid);
    +				unlock(&debuglock);
    +			}
    +			newosproc(m, m->g0, m->g0->stackbase, mstart);
    +		}
    +	}
    +}
    +
      // Scheduler loop: find g to run, run it, repeat.
      static void
      scheduler(void)
    

    matchmgは、GOMAXPROCSで指定されたCPU数までMを起動し、Gを割り当てる役割を担います。

  • sys·entersyscallsys·exitsyscallの新規追加:

    --- a/src/runtime/proc.c
    +++ b/src/runtime/proc.c
    @@ -428,23 +484,60 @@ sys·gosched(void)
      	}
      }
      
    -// Fork off a new m.  Sched must be locked.\n-static void\n-mnew(void)\n+// The goroutine g is about to enter a system call.\n+// Record that it's not using the cpu anymore.\n+// This is called only from the go syscall library, not\n+// from the low-level system calls used by the runtime.\n+// The "arguments" are syscall.Syscall's stack frame\n+void\nsys·entersyscall(uint64 callerpc, int64 trap)\n {\n    -	M *m;\n    +\tUSED(callerpc);\n    +\n    +\tif(debug > 1) {\n    +\t\tlock(&debuglock);\n    +\t\tprintf("m%d g%d enter syscall %D\n", m->id, g->goid, trap);\n    +\t\tunlock(&debuglock);\n    +\t}\n    +\tlock(&sched);\n    +\tsched.mcpu--;\n    +\tsched.msyscall++;\n    +\tif(sched.gwait != 0)\n    +\t\tmatchmg();\n    +\tunlock(&sched);\n    +}\n    +\n    +// The goroutine g exited its system call.\n    +// Arrange for it to run on a cpu again.\n    +// This is called only from the go syscall library, not\n    +// from the low-level system calls used by the runtime.\n    +void\nsys·exitsyscall(void)\n {\n    +\tif(debug > 1) {\n    +\t\tlock(&debuglock);\n    +\t\tprintf("m%d g%d exit syscall mcpu=%d mcpumax=%d\n", m->id, g->goid, sched.mcpu, sched.mcpumax);\n    +\t\tunlock(&debuglock);\n    +\t}\n    +\n    +\tlock(&sched);\n    +\tsched.msyscall--;\n    +\tsched.mcpu++;\n    +\t// Fast path - if there's room for this m, we're done.\n    +\tif(sched.mcpu <= sched.mcpumax) {\n    +\t\tunlock(&sched);\n    +\t\treturn;\n    +\t}\n    +\tunlock(&sched);\n    +\n    +\t// Slow path - all the cpus are taken.\n    +\t// The scheduler will ready g and put this m to sleep.\n    +\t// When the scheduler takes g awa from m,\n    +\t// it will undo the sched.mcpu++ above.\n    +\tsys·gosched();\n    +}\n    +\n    +\n     //\n     // the calling sequence for a routine tha\n     // needs N bytes stack, A args.\n    ```
    これらの関数は、システムコールへの出入りをランタイムに通知し、`sched.mcpu`と`sched.msyscall`を更新します。
    
    

src/lib/syscall/asm_amd64_darwin.s および src/lib/syscall/asm_amd64_linux.s

  • sys·entersyscallsys·exitsyscallの呼び出しの追加:
    --- a/src/lib/syscall/asm_amd64_darwin.s
    +++ b/src/lib/syscall/asm_amd64_darwin.s
    @@ -11,23 +11,28 @@
     // Trap # in AX, args in DI SI DX, return in AX DX
     
     TEXT	syscall·Syscall(SB),7,$0
    +	CALL	sys·entersyscall(SB)
     	MOVQ	16(SP), DI
     	MOVQ	24(SP), SI
     	MOVQ	32(SP), DX
     	MOVQ	8(SP), AX	// syscall entry
     	ADDQ	$0x2000000, AX
     	SYSCALL
    -	JCC	5(PC)
    +	JCC	ok
     	MOVQ	$-1, 40(SP)	// r1
     	MOVQ	$0, 48(SP)	// r2
     	MOVQ	AX, 56(SP)  // errno
    +	CALL	sys·exitsyscall(SB)
     	RET
    +ok:
     	MOVQ	AX, 40(SP)	// r1
     	MOVQ	DX, 48(SP)	// r2
     	MOVQ	$0, 56(SP)	// errno
    +	CALL	sys·exitsyscall(SB)
     	RET
     
     TEXT	syscall·Syscall6(SB),7,$0
    +	CALL	sys·entersyscall(SB)
     	MOVQ	16(SP), DI
     	MOVQ	24(SP), SI
     	MOVQ	32(SP), DX
    @@ -37,12 +42,15 @@ TEXT	syscall·Syscall6(SB),7,$0
     	MOVQ	8(SP), AX	// syscall entry
     	ADDQ	$0x2000000, AX
     	SYSCALL
    -	JCC	5(PC)
    +	JCC	ok6
     	MOVQ	$-1, 64(SP)	// r1
     	MOVQ	$0, 72(SP)	// r2
     	MOVQ	AX, 80(SP)  // errno
    +	CALL	sys·exitsyscall(SB)
     	RET
    +ok6:
     	MOVQ	AX, 64(SP)	// r1
     	MOVQ	DX, 72(SP)	// r2
     	MOVQ	$0, 80(SP)	// errno
    +	CALL	sys·exitsyscall(SB)
     	RET
    
    同様の変更がLinux版のアセンブリファイルにも適用されています。これにより、Goのシステムコールラッパーが、実際のシステムコール実行前後にランタイムのフックを呼び出すようになりました。

コアとなるコードの解説

このコミットの核心は、Goのスケジューラがシステムコールによるブロッキングをどのように扱うかという点にあります。

  1. GOMAXPROCSの新しい意味:

    • 以前はGOMAXPROCSがOSスレッドの最大数を直接制御していましたが、この変更により、Goランタイムが同時に利用できるCPUコアの論理的な最大数を指定するようになりました。これは、GoのM:Nスケジューリングモデルにおいて、P(プロセッサ)の数を制御することに相当します。
  2. システムコール時のMの切り離し:

    • ゴルーチンがシステムコール(例:ファイル読み書き、ネットワーク通信)を実行しようとすると、そのゴルーチンが現在実行されているOSスレッド(M)は、システムコールが完了するまでブロックされます。
    • このコミットでは、システムコールに入る直前にsys·entersyscallが、システムコールから戻った直後にsys·exitsyscallが呼び出されるようになりました。
    • sys·entersyscallが呼び出されると、ランタイムはsched.mcpu(CPUで実行中のMの数)をデクリメントし、sched.msyscall(システムコール中のMの数)をインクリメントします。これにより、このMが一時的にCPUを離れたことをスケジューラに伝えます。
    • もし、このMがCPUを離れたことで、sched.mcpusched.mcpumax(利用可能なCPU数)を下回った場合、matchmg関数が呼び出されます。matchmgは、待機中のゴルーチンがあれば、新しいOSスレッドを起動するか、既存のアイドル状態のOSスレッドにそのゴルーチンを割り当てて実行を開始させます。これにより、システムコールでブロックされたMがあっても、他のゴルーチンがCPUリソースを継続して利用できるようになります。
  3. システムコール終了時のMの再割り当て:

    • sys·exitsyscallが呼び出されると、ランタイムはsched.msyscallをデクリメントし、sched.mcpuをインクリメントします。
    • もしsched.mcpusched.mcpumaxを超えていなければ、このMはそのままCPU上でゴルーチンの実行を継続します。
    • しかし、もしsched.mcpusched.mcpumaxを超えてしまった場合(つまり、利用可能なCPU数よりも多くのMがCPU上で実行可能になった場合)、sys·gosched()が呼び出されます。これは、現在のMがゴルーチンを解放し、スケジューラに制御を戻すことを意味します。スケジューラは、このMをアイドル状態にするか、他のゴルーチンに割り当てるかを決定します。これにより、CPUリソースの過剰な利用を防ぎ、GOMAXPROCSで指定されたCPU数にMの実行を制限します。

このメカニズムにより、Goのランタイムは、システムコールによるブロッキングを「非ブロッキング」として扱い、CPUリソースを効率的に利用できるようになりました。これは、Goの並行処理モデルが、OSスレッドのブロッキングに起因するパフォーマンス問題を回避するための重要な進化です。

関連リンク

参考にした情報源リンク