[インデックス 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を実行できるようにするメカニズムを導入しようとしています。
技術的詳細
このコミットの主要な技術的変更点は以下の通りです。
-
GOMAXPROCS
のセマンティクス変更:src/runtime/proc.c
内のSched
構造体に、mmax
(最大スレッド数)からmcpumax
(最大CPU数)への変更が見られます。sched.mcpu
(現在CPUで実行中のMの数)とsched.msyscall
(システムコール中のMの数)という新しいフィールドが追加され、GoランタイムがCPU利用状況とシステムコールによるブロッキング状況をより詳細に追跡できるようになりました。schedinit
関数でGOMAXPROCS
環境変数を読み込み、sched.mcpumax
に設定するようになりました。
-
システムコール時のスケジューラ連携:
src/lib/syscall/asm_amd64_darwin.s
とsrc/lib/syscall/asm_amd64_linux.s
において、syscall.Syscall
およびsyscall.Syscall6
のラッパーにsys·entersyscall
とsys·exitsyscall
の呼び出しが追加されました。sys·entersyscall
が呼び出されると、現在のMがシステムコールに入ったことをランタイムに通知し、sched.mcpu
をデクリメントし、sched.msyscall
をインクリメントします。これにより、このMがCPUを占有していないことをスケジューラに伝えます。もし実行可能なゴルーチンが待機している場合、matchmg
関数が呼び出され、新しいMが起動されるか、既存のMがゴルーチンを実行するように促されます。sys·exitsyscall
が呼び出されると、システムコールから戻ったことをランタイムに通知し、sched.msyscall
をデクリメントし、sched.mcpu
をインクリメントします。これにより、このMが再びCPUを利用可能になったことをスケジューラに伝えます。もしsched.mcpu
がsched.mcpumax
を超えている場合(つまり、利用可能なCPU数を超えてMが実行可能になった場合)、sys·gosched()
が呼び出され、現在のMはゴルーチンを解放してスリープ状態に入る可能性があります。
-
スケジューラの改善:
src/runtime/proc.c
のready
関数とnextgandunlock
関数が変更され、sched.mcpu
とsched.mcpumax
を考慮してゴルーチンをスケジューリングするようになりました。matchmg
という新しい関数が導入されました。この関数は、sched.mcpu
がsched.mcpumax
よりも小さい場合に、待機中のゴルーチン(G)があれば、新しいOSスレッド(M)を起動するか、既存の待機中のMにGを割り当てて起動します。これにより、CPUリソースが最大限に活用されるようになります。scheduler
関数も、ゴルーチンの実行が完了した際にsched.mcpu
をデクリメントするロジックが追加されました。
-
デバッグ用出力関数の追加:
src/runtime/print.c
に、簡易版のprintf
関数とsys.printhex
関数が追加されました。これらはランタイム内部のデバッグ出力に使用されます。src/runtime/rt1_amd64_darwin.c
とsrc/runtime/rt1_amd64_linux.c
では、レジスタやアドレスの出力にsys.printpointer
の代わりにsys.printhex
を使用するように変更されました。
-
テストコードの変更:
test/dialgoogle.go
とtest/tcpserver.go
から、テスト実行時のGOMAXPROCS
の設定が削除されました。これは、GOMAXPROCS
のセマンティクス変更に伴い、テストが特定のOSスレッド数に依存する必要がなくなったためです。
これらの変更は、Goのスケジューラがシステムコールによるブロッキングを透過的に処理し、CPUリソースをより効率的に利用するための基盤を築くものであり、Goの並行処理モデルの進化における重要な一歩と言えます。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主にsrc/runtime/proc.c
と、システムコールをラップするアセンブリファイル(src/lib/syscall/asm_amd64_darwin.s
、src/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. };
mmax
がmcpumax
に変わり、mcpu
とmsyscall
が追加されました。 -
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.mcpu
とsched.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·entersyscall
とsys·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·entersyscall
とsys·exitsyscall
の呼び出しの追加:
同様の変更がLinux版のアセンブリファイルにも適用されています。これにより、Goのシステムコールラッパーが、実際のシステムコール実行前後にランタイムのフックを呼び出すようになりました。--- 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
コアとなるコードの解説
このコミットの核心は、Goのスケジューラがシステムコールによるブロッキングをどのように扱うかという点にあります。
-
GOMAXPROCS
の新しい意味:- 以前は
GOMAXPROCS
がOSスレッドの最大数を直接制御していましたが、この変更により、Goランタイムが同時に利用できるCPUコアの論理的な最大数を指定するようになりました。これは、GoのM:Nスケジューリングモデルにおいて、P(プロセッサ)の数を制御することに相当します。
- 以前は
-
システムコール時のMの切り離し:
- ゴルーチンがシステムコール(例:ファイル読み書き、ネットワーク通信)を実行しようとすると、そのゴルーチンが現在実行されているOSスレッド(M)は、システムコールが完了するまでブロックされます。
- このコミットでは、システムコールに入る直前に
sys·entersyscall
が、システムコールから戻った直後にsys·exitsyscall
が呼び出されるようになりました。 sys·entersyscall
が呼び出されると、ランタイムはsched.mcpu
(CPUで実行中のMの数)をデクリメントし、sched.msyscall
(システムコール中のMの数)をインクリメントします。これにより、このMが一時的にCPUを離れたことをスケジューラに伝えます。- もし、このMがCPUを離れたことで、
sched.mcpu
がsched.mcpumax
(利用可能なCPU数)を下回った場合、matchmg
関数が呼び出されます。matchmg
は、待機中のゴルーチンがあれば、新しいOSスレッドを起動するか、既存のアイドル状態のOSスレッドにそのゴルーチンを割り当てて実行を開始させます。これにより、システムコールでブロックされたMがあっても、他のゴルーチンがCPUリソースを継続して利用できるようになります。
-
システムコール終了時のMの再割り当て:
sys·exitsyscall
が呼び出されると、ランタイムはsched.msyscall
をデクリメントし、sched.mcpu
をインクリメントします。- もし
sched.mcpu
がsched.mcpumax
を超えていなければ、このMはそのままCPU上でゴルーチンの実行を継続します。 - しかし、もし
sched.mcpu
がsched.mcpumax
を超えてしまった場合(つまり、利用可能なCPU数よりも多くのMがCPU上で実行可能になった場合)、sys·gosched()
が呼び出されます。これは、現在のMがゴルーチンを解放し、スケジューラに制御を戻すことを意味します。スケジューラは、このMをアイドル状態にするか、他のゴルーチンに割り当てるかを決定します。これにより、CPUリソースの過剰な利用を防ぎ、GOMAXPROCS
で指定されたCPU数にMの実行を制限します。
このメカニズムにより、Goのランタイムは、システムコールによるブロッキングを「非ブロッキング」として扱い、CPUリソースを効率的に利用できるようになりました。これは、Goの並行処理モデルが、OSスレッドのブロッキングに起因するパフォーマンス問題を回避するための重要な進化です。
関連リンク
- Go言語の公式ドキュメント(スケジューラに関する現在の情報): https://go.dev/doc/effective_go#concurrency
- Goスケジューラの歴史と進化に関する記事(非公式なものも含む):
- The Go scheduler: https://go.dev/blog/go15scheduler (Go 1.5での大きな変更に関する記事ですが、背景理解に役立ちます)
- Go's work-stealing scheduler: https://rakyll.org/scheduler/
- システムコールに関する一般的な情報: https://ja.wikipedia.org/wiki/%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%82%B3%E3%83%BC%E3%83%AB
参考にした情報源リンク
- Go言語のソースコード (特に
src/runtime
ディレクトリ): https://github.com/golang/go - Go言語の初期のコミット履歴: https://github.com/golang/go/commits/master
- Go言語の設計に関する議論(メーリングリストやデザインドキュメントなど、当時の情報源)
- Go Nutsメーリングリストアーカイブ: https://groups.google.com/g/golang-nuts
- Go言語のスケジューラに関する技術ブログや解説記事(このコミットの時期に直接言及しているものは少ないが、Goスケジューラの進化を理解する上で参考になるもの)
- Go scheduler: M, P, G: https://medium.com/a-journey-with-go/go-scheduler-m-p-g-65306297445c
- Understanding Go's Concurrency Model: https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part1.html
- Go's Concurrency Primitives: Goroutines and Channels: https://www.digitalocean.com/community/tutorials/understanding-go-concurrency-goroutines-and-channels