[インデックス 15393] ファイルの概要
このコミットは、Go言語のランタイムにおける細かな変更を記録しています。特に、新しいスケジューラの導入に伴う差分を最小限に抑えることを目的としています。
コミット
runtime: minor changes to minimize diffs of new scheduler
R=golang-dev, rsc CC=golang-dev https://golang.org/cl/7381048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1d7faf91dfe6aaa5f43b74b19bc014937ea92337
元コミット内容
commit 1d7faf91dfe6aaa5f43b74b19bc014937ea92337
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Sat Feb 23 08:39:31 2013 +0400
runtime: minor changes
to minimize diffs of new scheduler
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7381048
変更の背景
このコミットの主な背景は、Go言語のランタイムに新しいスケジューラが導入される準備です。コミットメッセージにある「to minimize diffs of new scheduler」という記述がその意図を明確に示しています。
2013年頃のGo言語のランタイムは、スケジューラの大幅な改善が行われていました。特に、Dmitriy Vyukov氏によって提案・実装された新しいスケジューラ(一般に「M:Nスケジューラ」として知られる)は、Goの並行処理モデルの効率とスケーラビリティを大きく向上させるものでした。この新しいスケジューラは、既存のスケジューラと比較して、より多くのOSスレッド(M)上でより多くのゴルーチン(G)を効率的に実行できるように設計されていました。
このような大規模な変更を導入する際には、一度にすべての変更を適用すると、コードレビューが困難になり、バグの特定も難しくなります。そのため、関連する小さな変更を事前にコミットし、新しいスケジューラの本体のコミットがよりクリーンで理解しやすいものになるように準備を進めるのが一般的な開発プラクティスです。
このコミットは、新しいスケジューラが導入された際に発生するであろう差分(diff)を最小限に抑えるために、既存のコードベースに対して行われた「マイナーな変更」を集めたものです。具体的には、新しいスケジューラのロジックと衝突しないように、あるいは新しいスケジューラが依存する可能性のある既存のランタイム関数の振る舞いを微調整するために行われたと考えられます。
前提知識の解説
このコミットを理解するためには、以下のGo言語のランタイムと並行処理に関する基本的な知識が必要です。
Goランタイム (Go Runtime)
Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。Goランタイムは、ガベージコレクション、スケジューリング、メモリ管理、システムコールインターフェースなど、Goプログラムの実行に必要な多くの低レベルな機能を提供します。C言語で書かれた部分が多く、src/pkg/runtime
ディレクトリにそのソースコードがあります。
ゴルーチン (Goroutine)
ゴルーチンはGo言語の並行処理の基本単位です。OSスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。ゴルーチンはGoランタイムによってスケジューリングされ、OSスレッドにマッピングされます。
M:P:G モデル (Machine:Processor:Goroutine Model)
Goのスケジューラは、M:P:Gモデルと呼ばれる抽象化されたモデルで動作します。
- G (Goroutine): 実行されるコードの単位。Go関数呼び出しによって作成されます。
- P (Processor): 論理的なプロセッサ。OSスレッド(M)とゴルーチン(G)の間の仲介役を果たします。Pは実行可能なゴルーチンのキューを持ち、Mにゴルーチンをディスパッチします。
GOMAXPROCS
環境変数によってPの数が制御されます。 - M (Machine): OSスレッド。Pに割り当てられたゴルーチンを実行します。MはOSによってスケジューリングされます。
このモデルにより、GoランタイムはOSスレッドの数を制限しつつ、多数のゴルーチンを効率的に並行実行できます。
スケジューラ (Scheduler)
Goランタイムのスケジューラは、ゴルーチンをPに割り当て、PがM上でゴルーチンを実行するプロセスを管理します。ゴルーチンがブロックされたり、I/O操作を行ったり、タイムスライスを使い切ったりすると、スケジューラは別の実行可能なゴルーチンをMに割り当てます。
ガベージコレクション (Garbage Collection, GC)
Goは自動メモリ管理(ガベージコレクション)を採用しています。GCは、プログラムが使用しなくなったメモリを自動的に解放するプロセスです。GCが実行されている間、プログラムの実行が一時停止(Stop-the-World)することがあります。GCの効率は、Goプログラムのパフォーマンスに大きく影響します。
proc.c
src/pkg/runtime/proc.c
は、Goランタイムのスケジューリング、ゴルーチン管理、M:P:Gモデルのコアロジックを含む重要なファイルです。このファイルへの変更は、ランタイムの根幹に関わるものです。
技術的詳細
このコミットは、src/pkg/runtime/proc.c
ファイルに対して行われた複数の小さな変更を含んでいます。これらの変更は、新しいスケジューラの導入に備えて、既存のランタイムの挙動を微調整することを目的としています。
1. runtime·main
関数の変更
runtime·main
はGoプログラムのエントリポイントであり、ランタイムの初期化を行います。
追加された行:
+ if(m != &runtime·m0)
+ runtime·throw("runtime·main not on m0");
これは、runtime·main
関数が常に初期OSスレッドであるruntime·m0
上で実行されることを保証するためのチェックです。もしruntime·main
がm0
以外のMで実行された場合、ランタイムエラーを発生させます。これは、ランタイムの初期化プロセスが特定のMに依存していることを示唆しており、新しいスケジューラがMの管理方法を変更する際に、この前提が崩れないようにするための防御的なチェックと考えられます。
2. runtime·gcprocs
関数の変更
runtime·gcprocs
は、ガベージコレクション中に使用するCPUの数を決定する関数です。
変更点:
runtime·lock(&runtime·sched);
とruntime·unlock(&runtime·sched);
が追加されました。 これは、runtime·sched
構造体(スケジューラの状態を保持する)へのアクセスをロックで保護することで、並行性に関する問題を回避するための変更です。GCプロセスの数を計算する際に、スケジューラの状態が他のゴルーチンによって変更されないようにするため、スレッドセーフティを確保しています。
3. needaddgcproc
関数の追加
新しい静的関数needaddgcproc
が追加されました。
+static bool
+needaddgcproc(void)
+{
+ int32 n;
+
+ runtime·lock(&runtime·sched);
+ n = runtime·gomaxprocs;
+ if(n > runtime·ncpu)
+ n = runtime·ncpu;
+ if(n > MaxGcproc)
+ n = MaxGcproc;
+ n -= runtime·sched.mwait+1; // one M is currently running
+ runtime·unlock(&runtime·sched);
+ return n > 0;
+}
この関数は、GCヘルパープロセッサ(M)を追加する必要があるかどうかを判断します。runtime·gcprocs
と同様のロジックで、gomaxprocs
、ncpu
、MaxGcproc
に基づいて必要なGCプロセッサ数を計算し、現在待機中のMの数と比較します。この関数は、GC中に利用可能なMが不足している場合に、新しいMを起動する必要があるかを判断するために使用されます。これは、GCの効率を向上させるためのスケジューラの最適化の一部と考えられます。
4. runtime·starttheworld
関数の変更
runtime·starttheworld
は、GCのStop-the-Worldフェーズが終了し、ゴルーチンの実行が再開される際に呼び出される関数です。
変更点:
- GCプロセッサ数の計算ロジックが
needaddgcproc()
の呼び出しに置き換えられました。 if(runtime·gcprocs() < max && canaddmcpu())
がif(add && canaddmcpu())
に変更されました。 これにより、GCヘルパープロセッサを追加する必要があるかどうかの判断が、新しく導入されたneedaddgcproc
関数に委譲されました。これにより、コードの重複が避けられ、GCプロセッサの管理ロジックが一元化されます。これは、新しいスケジューラがGC中のMの管理をより細かく制御するための準備と考えられます。
5. runtime·allocm
, runtime·needm
, lockextra
, runtime·newm
関数のコメントとフォーマットの変更
これらの関数では、主にコメントの修正や空白の調整が行われています。例えば、runtime·needm
関数では、コメント内の改行が修正され、より読みやすくなっています。
- // allocation until then so that it can be done \n
+ // allocation until then so that it can be done\n
このような変更は、機能的な変更ではなく、コードの可読性を向上させ、新しいスケジューラの導入による大規模な差分を最小限に抑えるための「マイナーな変更」の典型です。
6. schedule
関数の変更
schedule
関数は、ゴルーチンをスケジューリングするコアロジックの一部です。
変更点:
- if(gp->sched.pc == (byte*)runtime·goexit) {\t// kickoff
+ if(gp->sched.pc == (byte*)runtime·goexit) // kickoff
\t\truntime·gogocallfn(&gp->sched, gp->fnstart);\n
-\t}\n
if
文のブロックを囲む波括弧が削除されました。これは、単一のステートメントのみを含むif
文の場合に波括弧を省略するというGoのコーディングスタイルに合わせた変更である可能性があります。機能的な影響はありませんが、コードの整形と一貫性を保つための変更です。
7. runtime·NumGoroutine
とruntime·gcount
関数の変更
runtime·NumGoroutine
は、現在実行中のゴルーチンの数を返す関数です。
変更点:
runtime·NumGoroutine
がruntime·sched.gcount
を直接参照する代わりに、新しく導入されたruntime·gcount()
関数を呼び出すように変更されました。runtime·gcount()
関数が新しく実装されました。
+int32
+runtime·gcount(void)
+{
+ G *gp;
+ int32 n, s;
+
+ n = 0;
+ runtime·lock(&runtime·sched);
+ for(gp = runtime·allg; gp; gp = gp->alllink) {
+ s = gp->status;
+ if(s == Grunnable || s == Grunning || s == Gsyscall || s == Gwaiting)
+ n++;
+ }
+ runtime·unlock(&runtime·sched);
+ return n;
+}
以前はruntime·sched.gcount
というカウンタがゴルーチン数を保持していましたが、新しいruntime·gcount()
関数は、runtime·allg
リンクリストを走査し、Grunnable
、Grunning
、Gsyscall
、Gwaiting
の状態にあるゴルーチンを数えることで、動的にゴルーチン数を計算します。この変更は、ゴルーチン数のカウント方法をより正確かつ堅牢にするためのものです。特に、新しいスケジューラがゴルーチンの状態遷移や管理方法を変更する際に、静的なカウンタよりも動的な計算の方が信頼性が高いため、このような変更が行われたと考えられます。また、runtime·sched
へのロックが追加され、スレッドセーフティが確保されています。
8. runtime·sigprof
関数の変更
runtime·sigprof
は、プロファイリングのためにシグナルが受信された際に呼び出される関数です。
追加された行:
+ if(m == nil || m->mcache == nil)
+ return;
これは、m
(現在のM)またはm->mcache
(Mのキャッシュ)がnil
である場合に、関数を早期に終了させるための防御的なチェックです。プロファイリング中にこれらのポインタが不正な状態である可能性を考慮し、クラッシュを防ぐための堅牢性向上策と考えられます。
これらの変更は、個々には小さいものですが、全体として新しいスケジューラがGoランタイムに統合される際の摩擦を減らし、よりスムーズな移行を可能にするための重要な準備作業であったと言えます。
コアとなるコードの変更箇所
diff --git a/src/pkg/runtime/proc.c b/src/pkg/runtime/proc.c
index e2ba4b6614..f1e3ad59d7 100644
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -239,6 +239,8 @@ runtime·main(void)\n \t// by calling runtime.LockOSThread during initialization\n \t// to preserve the lock.\n \truntime·lockOSThread();\n+\tif(m != &runtime·m0)\n+\t\truntime·throw("runtime·main not on m0");\n \t// From now on, newgoroutines may use non-main threads.\n \tsetmcpumax(runtime·gomaxprocs);\n \truntime·sched.init = true;\
@@ -255,7 +257,7 @@ runtime·main(void)\n \tmain·main();\n \tif(raceenabled)\n \t\truntime·racefini();\n-\t\n+\n \t// Make racy client program work: if panicking on\n \t// another goroutine at the same time as main returns,\n \t// let the other goroutine finish printing the panic trace.\n@@ -669,9 +671,10 @@ int32\n runtime·gcprocs(void)\n {\n \tint32 n;\n-\t\n+\n \t// Figure out how many CPUs to use during GC.\n \t// Limited by gomaxprocs, number of actual CPUs, and MaxGcproc.\n+\truntime·lock(&runtime·sched);\n \tn = runtime·gomaxprocs;\n \tif(n > runtime·ncpu)\n \t\tn = runtime·ncpu;\
@@ -679,9 +682,26 @@ runtime·gcprocs(void)\n \t\tn = MaxGcproc;\n \tif(n > runtime·sched.mwait+1) // one M is currently running\n \t\tn = runtime·sched.mwait+1;\n+\truntime·unlock(&runtime·sched);\n \treturn n;\n }\n \n+static bool\n+needaddgcproc(void)\n+{\\n+\tint32 n;\n+\n+\truntime·lock(&runtime·sched);\n+\tn = runtime·gomaxprocs;\n+\tif(n > runtime·ncpu)\n+\t\tn = runtime·ncpu;\n+\tif(n > MaxGcproc)\n+\t\tn = MaxGcproc;\n+\tn -= runtime·sched.mwait+1; // one M is currently running\n+\truntime·unlock(&runtime·sched);\n+\treturn n > 0;\n+}\n+\n void\n runtime·helpgc(int32 nproc)\n {\n@@ -740,20 +760,14 @@ void\n runtime·starttheworld(void)\n {\n \tM *mp;\n-\tint32 max;\n-\t\n-\t// Figure out how many CPUs GC could possibly use.\n-\tmax = runtime·gomaxprocs;\n-\tif(max > runtime·ncpu)\n-\t\tmax = runtime·ncpu;\n-\tif(max > MaxGcproc)\n-\t\tmax = MaxGcproc;\n+\tbool add;\n \n+\tadd = needaddgcproc();\n \tschedlock();\n \truntime·gcwaiting = 0;\n \tsetmcpumax(runtime·gomaxprocs);\n \tmatchmg();\n-\tif(runtime·gcprocs() < max && canaddmcpu()) {\n+\tif(add && canaddmcpu()) {\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 \t\t// It would have been even better to start it before the collection,\n@@ -866,7 +880,7 @@ runtime·allocm(void)\n \t\tmp->g0 = runtime·malg(-1);\n \telse\n \t\tmp->g0 = runtime·malg(8192);\n-\t\n+\n \treturn mp;\n }\n \n@@ -921,13 +935,13 @@ runtime·needm(byte x)\n \t// Set needextram when we\'ve just emptied the list,\n \t// so that the eventual call into cgocallbackg will\n \t// allocate a new m for the extra list. We delay the\n-\t// allocation until then so that it can be done \n+\t// allocation until then so that it can be done\n \t// after exitsyscall makes sure it is okay to be\n \t// running at all (that is, there\'s no garbage collection\n-\t// running right now).\t\n+\t// running right now).\n \tmp->needextram = mp->schedlink == nil;\n \tunlockextra(mp->schedlink);\n-\t\n+\n \t// Install m and g (= m->g0) and set the stack bounds\n \t// to match the current stack. We don\'t actually know\n \t// how big the stack is, like we don\'t know how big any\n@@ -995,7 +1009,7 @@ runtime·newextram(void)\n // The main expense here is the call to signalstack to release the\n // m\'s signal stack, and then the call to needm on the next callback\n // from this thread. It is tempting to try to save the m for next time,\n-// which would eliminate both these costs, but there might not be \n+// which would eliminate both these costs, but there might not be\n // a next time: the current thread (which Go does not control) might exit.\n // If we saved the m for that thread, there would be an m leak each time\n // such a thread exited. Instead, we acquire and release an m on each\n@@ -1042,7 +1056,7 @@ lockextra(bool nilokay)\n {\n \tM *mp;\n \tvoid (*yield)(void);\n-\t\n+\n \tfor(;;) {\n \t\tmp = runtime·atomicloadp(&runtime·extram);\n \t\tif(mp == MLOCKED) {\n@@ -1077,7 +1091,7 @@ M*\n runtime·newm(void)\n {\n \tM *mp;\n-\t\n+\n \tmp = runtime·allocm();\n \n \tif(runtime·iscgo) {\n@@ -1171,9 +1185,8 @@ schedule(G *gp)\n \tif(m->profilehz != hz)\n \t\truntime·resetcpuprofiler(hz);\n \n-\tif(gp->sched.pc == (byte*)runtime·goexit) {\t// kickoff\n+\tif(gp->sched.pc == (byte*)runtime·goexit) // kickoff\n \t\truntime·gogocallfn(&gp->sched, gp->fnstart);\n-\t}\n \truntime·gogo(&gp->sched, 0);\n }\n \n@@ -1603,7 +1616,7 @@ UnlockOSThread(void)\n \t\treturn;\n \tm->lockedg = nil;\n \tg->lockedm = nil;\n-}\t\n+}\n \n void\n runtime·UnlockOSThread(void)\n@@ -1646,14 +1659,25 @@ runtime·mid(uint32 ret)\n void\n runtime·NumGoroutine(intgo ret)\n {\n-\tret = runtime·sched.gcount;\n+\tret = runtime·gcount();\n \tFLUSH(&ret);\n }\n \n int32\n runtime·gcount(void)\n {\n-\treturn runtime·sched.gcount;\n+\tG *gp;\n+\tint32 n, s;\n+\n+\tn = 0;\n+\truntime·lock(&runtime·sched);\n+\tfor(gp = runtime·allg; gp; gp = gp->alllink) {\n+\t\ts = gp->status;\n+\t\tif(s == Grunnable || s == Grunning || s == Gsyscall || s == Gwaiting)\n+\t\t\tn++;\n+\t}\n+\truntime·unlock(&runtime·sched);\n+\treturn n;\n }\n \n int32\n@@ -1687,6 +1711,8 @@ runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp)\n {\n \tint32 n;\n \n+\tif(m == nil || m->mcache == nil)\n+\t\treturn;\n \tif(prof.fn == nil || prof.hz == 0)\n \t\treturn;\n \n```
## コアとなるコードの解説
### `runtime·main`関数の変更
```c
+\tif(m != &runtime·m0)\n+\t\truntime·throw("runtime·main not on m0");
このコードは、Goプログラムの起動時にruntime·main
関数が、ランタイムが最初に起動するOSスレッドであるruntime·m0
上で実行されていることを確認します。もしm
(現在のOSスレッド)がruntime·m0
と異なる場合、runtime·throw
を呼び出して致命的なエラーを発生させます。これは、ランタイムの初期化が特定のOSスレッドに強く依存しているため、その前提が崩れないようにするための防御的なチェックです。新しいスケジューラがOSスレッドの管理方法を変更する際に、この初期化の制約が維持されることを保証する意図があります。
runtime·gcprocs
関数の変更
+\truntime·lock(&runtime·sched);\n \tn = runtime·gomaxprocs;\n \tif(n > runtime·ncpu)\n \t\tn = runtime·ncpu;\n \tif(n > MaxGcproc)\n \t\tn = MaxGcproc;\n \tif(n > runtime·sched.mwait+1) // one M is currently running\n \t\tn = runtime·sched.mwait+1;\n+\truntime·unlock(&runtime·sched);\
runtime·gcprocs
関数は、ガベージコレクション中に使用するCPUの数を決定します。この変更では、runtime·sched
構造体へのアクセスをruntime·lock
とruntime·unlock
で囲むことで、スレッドセーフティを確保しています。runtime·sched
はスケジューラのグローバルな状態を保持しており、複数のゴルーチンやMから同時にアクセスされる可能性があります。GCプロセスの数を計算する際に、この共有データが競合状態によって不正な値になることを防ぐために、排他制御が導入されました。これは、新しいスケジューラが並行性をより重視する設計になっていることと関連している可能性があります。
needaddgcproc
関数の追加
+static bool\n+needaddgcproc(void)\n+{\n+\tint32 n;\n+\n+\truntime·lock(&runtime·sched);\n+\tn = runtime·gomaxprocs;\n+\tif(n > runtime·ncpu)\n+\t\tn = runtime·ncpu;\n+\tif(n > MaxGcproc)\n+\t\tn = MaxGcproc;\n+\tn -= runtime·sched.mwait+1; // one M is currently running\n+\truntime·unlock(&runtime·sched);\n+\treturn n > 0;\n+}\
この新しい関数は、GCヘルパープロセッサ(M)を追加する必要があるかどうかを判断します。runtime·gcprocs
と同様のロジックで、GOMAXPROCS
、利用可能なCPU数、GCプロセッサの最大数に基づいて、GCに利用できるMの理想的な数を計算します。そして、現在待機中のMの数と比較し、追加のMが必要であればtrue
を返します。この関数は、GCの効率を向上させるために、必要に応じてMを動的に起動するスケジューラのロジックをカプセル化するために導入されました。
runtime·starttheworld
関数の変更
-\tint32 max;\n-\t\n-\t// Figure out how many CPUs GC could possibly use.\n-\tmax = runtime·gomaxprocs;\n-\tif(max > runtime·ncpu)\n-\t\tmax = runtime·ncpu;\n-\tif(max > MaxGcproc)\n-\t\tmax = MaxGcproc;\n+\tbool add;\n \n+\tadd = needaddgcproc();\n \tschedlock();\n \truntime·gcwaiting = 0;\n \tsetmcpumax(runtime·gomaxprocs);\n \tmatchmg();\n-\tif(runtime·gcprocs() < max && canaddmcpu()) {\n+\tif(add && canaddmcpu()) {\
runtime·starttheworld
関数は、GCのStop-the-Worldフェーズが終了し、ゴルーチンの実行が再開される際に呼び出されます。この変更では、GCヘルパープロセッサを追加する必要があるかどうかの判断ロジックが、新しく導入されたneedaddgcproc()
関数に置き換えられました。これにより、コードの重複が排除され、GCプロセッサの管理ロジックが一元化されます。これは、新しいスケジューラがGC中のMの管理をより効率的かつ柔軟に行うための準備です。
runtime·NumGoroutine
とruntime·gcount
関数の変更
-\tret = runtime·sched.gcount;\n+\tret = runtime·gcount();\n \tFLUSH(&ret);\n }\n \n int32\n runtime·gcount(void)\n {\n-\treturn runtime·sched.gcount;\n+\tG *gp;\n+\tint32 n, s;\n+\n+\tn = 0;\n+\truntime·lock(&runtime·sched);\n+\tfor(gp = runtime·allg; gp; gp = gp->alllink) {\n+\t\ts = gp->status;\n+\t\tif(s == Grunnable || s == Grunning || s == Gsyscall || s == Gwaiting)\n+\t\t\tn++;\n+\t}\n+\truntime·unlock(&runtime·sched);\n+\treturn n;\n}\
以前はruntime·NumGoroutine
がruntime·sched.gcount
という静的なカウンタを直接参照してゴルーチン数を取得していましたが、この変更により、新しく実装されたruntime·gcount()
関数を呼び出すようになりました。runtime·gcount()
関数は、runtime·allg
リンクリストを走査し、Grunnable
(実行可能)、Grunning
(実行中)、Gsyscall
(システムコール中)、Gwaiting
(待機中)の状態にあるゴルーチンを動的に数え上げます。この変更は、ゴルーチン数のカウント方法をより正確かつ堅牢にするためのものです。特に、新しいスケジューラがゴルーチンの状態遷移や管理方法を変更する際に、静的なカウンタよりも動的な計算の方が信頼性が高いため、このような変更が行われたと考えられます。また、runtime·sched
へのロックが追加され、スレッドセーフティが確保されています。
runtime·sigprof
関数の変更
+\tif(m == nil || m->mcache == nil)\n+\t\treturn;\
runtime·sigprof
関数は、プロファイリングのためにシグナルが受信された際に呼び出されます。この変更では、m
(現在のOSスレッド)またはm->mcache
(Mのキャッシュ)がnil
である場合に、関数を早期に終了させるための防御的なチェックが追加されました。これは、プロファイリング中にこれらのポインタが不正な状態である可能性を考慮し、ランタイムのクラッシュを防ぐための堅牢性向上策です。
関連リンク
参考にした情報源リンク
- Go Scheduler: https://go.dev/doc/articles/go_scheduler.html
- Go's new scheduler: https://go.dev/blog/go-concurrency-patterns-pipelines (直接的な記事ではないが、当時の並行処理の文脈を理解するのに役立つ)
- Go runtime source code: https://github.com/golang/go/tree/master/src/runtime
- Dmitriy Vyukov's contributions to Go: (一般的な検索で彼のスケジューラ関連の貢献が見つかる)
- GoのM:P:Gモデルに関する解説記事 (多数存在するため、特定のURLは挙げないが、概念理解に利用)
- Goのガベージコレクションに関する解説記事 (多数存在するため、特定のURLは挙げないが、概念理解に利用)
- Goの
proc.c
ファイルに関する議論やドキュメント (当時のGoコミュニティの議論や設計ドキュメントを参照) - Goのコミット履歴と関連するコードレビュー (GitHubの履歴を遡って関連するコミットや議論を確認)
[インデックス 15393] ファイルの概要
このコミットは、Go言語のランタイムにおける細かな変更を記録しています。特に、新しいスケジューラの導入に伴う差分を最小限に抑えることを目的としています。
コミット
runtime: minor changes to minimize diffs of new scheduler
R=golang-dev, rsc CC=golang-dev https://golang.org/cl/7381048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1d7faf91dfe6aaa5f43b74b19bc014937ea92337
元コミット内容
commit 1d7faf91dfe6aaa5f43b74b19bc014937ea92337
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Sat Feb 23 08:39:31 2013 +0400
runtime: minor changes
to minimize diffs of new scheduler
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7381048
変更の背景
このコミットの主な背景は、Go言語のランタイムに新しいスケジューラが導入される準備です。コミットメッセージにある「to minimize diffs of new scheduler」という記述がその意図を明確に示しています。
2013年頃のGo言語のランタイムは、スケジューラの大幅な改善が行われていました。特に、Dmitriy Vyukov氏によって提案・実装された新しいスケジューラ(一般に「M:Nスケジューラ」として知られる)は、Goの並行処理モデルの効率とスケーラビリティを大きく向上させるものでした。この新しいスケジューラは、既存のスケジューラと比較して、より多くのOSスレッド(M)上でより多くのゴルーチン(G)を効率的に実行できるように設計されていました。
このような大規模な変更を導入する際には、一度にすべての変更を適用すると、コードレビューが困難になり、バグの特定も難しくなります。そのため、関連する小さな変更を事前にコミットし、新しいスケジューラの本体のコミットがよりクリーンで理解しやすいものになるように準備を進めるのが一般的な開発プラクティスです。
このコミットは、新しいスケジューラが導入された際に発生するであろう差分(diff)を最小限に抑えるために、既存のコードベースに対して行われた「マイナーな変更」を集めたものです。具体的には、新しいスケジューラのロジックと衝突しないように、あるいは新しいスケジューラが依存する可能性のある既存のランタイム関数の振る舞いを微調整するために行われたと考えられます。
前提知識の解説
このコミットを理解するためには、以下のGo言語のランタイムと並行処理に関する基本的な知識が必要です。
Goランタイム (Go Runtime)
Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。Goランタイムは、ガベージコレクション、スケジューリング、メモリ管理、システムコールインターフェースなど、Goプログラムの実行に必要な多くの低レベルな機能を提供します。C言語で書かれた部分が多く、src/pkg/runtime
ディレクトリにそのソースコードがあります。
ゴルーチン (Goroutine)
ゴルーチンはGo言語の並行処理の基本単位です。OSスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。ゴルーチンはGoランタイムによってスケジューリングされ、OSスレッドにマッピングされます。
M:P:G モデル (Machine:Processor:Goroutine Model)
Goのスケジューラは、M:P:Gモデルと呼ばれる抽象化されたモデルで動作します。
- G (Goroutine): 実行されるコードの単位。Go関数呼び出しによって作成されます。
- P (Processor): 論理的なプロセッサ。OSスレッド(M)とゴルーチン(G)の間の仲介役を果たします。Pは実行可能なゴルーチンのキューを持ち、Mにゴルーチンをディスパッチします。
GOMAXPROCS
環境変数によってPの数が制御されます。 - M (Machine): OSスレッド。Pに割り当てられたゴルーチンを実行します。MはOSによってスケジューリングされます。
このモデルにより、GoランタイムはOSスレッドの数を制限しつつ、多数のゴルーチンを効率的に並行実行できます。
スケジューラ (Scheduler)
Goランタイムのスケジューラは、ゴルーチンをPに割り当て、PがM上でゴルーチンを実行するプロセスを管理します。ゴルーチンがブロックされたり、I/O操作を行ったり、タイムスライスを使い切ったりすると、スケジューラは別の実行可能なゴルーチンをMに割り当てます。
ガベージコレクション (Garbage Collection, GC)
Goは自動メモリ管理(ガベージコレクション)を採用しています。GCは、プログラムが使用しなくなったメモリを自動的に解放するプロセスです。GCが実行されている間、プログラムの実行が一時停止(Stop-the-World)することがあります。GCの効率は、Goプログラムのパフォーマンスに大きく影響します。
proc.c
src/pkg/runtime/proc.c
は、Goランタイムのスケジューリング、ゴルーチン管理、M:P:Gモデルのコアロジックを含む重要なファイルです。このファイルへの変更は、ランタイムの根幹に関わるものです。
技術的詳細
このコミットは、src/pkg/runtime/proc.c
ファイルに対して行われた複数の小さな変更を含んでいます。これらの変更は、新しいスケジューラの導入に備えて、既存のランタイムの挙動を微調整することを目的としています。
1. runtime·main
関数の変更
runtime·main
はGoプログラムのエントリポイントであり、ランタイムの初期化を行います。
追加された行:
+ if(m != &runtime·m0)
+ runtime·throw("runtime·main not on m0");
これは、runtime·main
関数が常に初期OSスレッドであるruntime·m0
上で実行されることを保証するためのチェックです。もしruntime·main
がm0
以外のMで実行された場合、ランタイムエラーを発生させます。これは、ランタイムの初期化プロセスが特定のMに依存していることを示唆しており、新しいスケジューラがMの管理方法を変更する際に、この前提が崩れないようにするための防御的なチェックと考えられます。
2. runtime·gcprocs
関数の変更
runtime·gcprocs
は、ガベージコレクション中に使用するCPUの数を決定する関数です。
変更点:
runtime·lock(&runtime·sched);
とruntime·unlock(&runtime·sched);
が追加されました。 これは、runtime·sched
構造体(スケジューラの状態を保持する)へのアクセスをロックで保護することで、並行性に関する問題を回避するための変更です。GCプロセスの数を計算する際に、スケジューラの状態が他のゴルーチンによって変更されないようにするため、スレッドセーフティを確保しています。
3. needaddgcproc
関数の追加
新しい静的関数needaddgcproc
が追加されました。
+static bool
+needaddgcproc(void)
+{
+ int32 n;
+
+ runtime·lock(&runtime·sched);
+ n = runtime·gomaxprocs;
+ if(n > runtime·ncpu)
+ n = runtime·ncpu;
+ if(n > MaxGcproc)
+ n = MaxGcproc;
+ n -= runtime·sched.mwait+1; // one M is currently running
+ runtime·unlock(&runtime·sched);
+ return n > 0;
+}
この関数は、GCヘルパープロセッサ(M)を追加する必要があるかどうかを判断します。runtime·gcprocs
と同様のロジックで、gomaxprocs
、ncpu
、MaxGcproc
に基づいて必要なGCプロセッサ数を計算し、現在待機中のMの数と比較します。この関数は、GC中に利用可能なMが不足している場合に、新しいMを起動する必要があるかを判断するために使用されます。これは、GCの効率を向上させるためのスケジューラの最適化の一部と考えられます。
4. runtime·starttheworld
関数の変更
runtime·starttheworld
は、GCのStop-the-Worldフェーズが終了し、ゴルーチンの実行が再開される際に呼び出される関数です。
変更点:
- GCプロセッサ数の計算ロジックが
needaddgcproc()
の呼び出しに置き換えられました。 if(runtime·gcprocs() < max && canaddmcpu())
がif(add && canaddmcpu())
に変更されました。 これにより、GCヘルパープロセッサを追加する必要があるかどうかの判断が、新しく導入されたneedaddgcproc
関数に委譲されました。これにより、コードの重複が避けられ、GCプロセッサの管理ロジックが一元化されます。これは、新しいスケジューラがGC中のMの管理をより細かく制御するための準備と考えられます。
5. runtime·allocm
, runtime·needm
, lockextra
, runtime·newm
関数のコメントとフォーマットの変更
これらの関数では、主にコメントの修正や空白の調整が行われています。例えば、runtime·needm
関数では、コメント内の改行が修正され、より読みやすくなっています。
- // allocation until then so that it can be done \n
+ // allocation until then so that it can be done\n
このような変更は、機能的な変更ではなく、コードの可読性を向上させ、新しいスケジューラの導入による大規模な差分を最小限に抑えるための「マイナーな変更」の典型です。
6. schedule
関数の変更
schedule
関数は、ゴルーチンをスケジューリングするコアロジックの一部です。
変更点:
- if(gp->sched.pc == (byte*)runtime·goexit) {\t// kickoff
+ if(gp->sched.pc == (byte*)runtime·goexit) // kickoff
\t\truntime·gogocallfn(&gp->sched, gp->fnstart);\n
-\t}\n
if
文のブロックを囲む波括弧が削除されました。これは、単一のステートメントのみを含むif
文の場合に波括弧を省略するというGoのコーディングスタイルに合わせた変更である可能性があります。機能的な影響はありませんが、コードの整形と一貫性を保つための変更です。
7. runtime·NumGoroutine
とruntime·gcount
関数の変更
runtime·NumGoroutine
は、現在実行中のゴルーチンの数を返す関数です。
変更点:
runtime·NumGoroutine
がruntime·sched.gcount
を直接参照する代わりに、新しく導入されたruntime·gcount()
関数を呼び出すように変更されました。runtime·gcount()
関数が新しく実装されました。
+int32
+runtime·gcount(void)
+{
+ G *gp;
+ int32 n, s;
+
+ n = 0;
+ runtime·lock(&runtime·sched);
+ for(gp = runtime·allg; gp; gp = gp->alllink) {
+ s = gp->status;
+ if(s == Grunnable || s == Grunning || s == Gsyscall || s == Gwaiting)
+ n++;
+ }
+ runtime·unlock(&runtime·sched);
+ return n;
+}
以前はruntime·sched.gcount
というカウンタがゴルーチン数を保持していましたが、新しいruntime·gcount()
関数は、runtime·allg
リンクリストを走査し、Grunnable
、Grunning
、Gsyscall
、Gwaiting
の状態にあるゴルーチンを数えることで、動的にゴルーチン数を計算します。この変更は、ゴルーチン数のカウント方法をより正確かつ堅牢にするためのものです。特に、新しいスケジューラがゴルーチンの状態遷移や管理方法を変更する際に、静的なカウンタよりも動的な計算の方が信頼性が高いため、このような変更が行われたと考えられます。また、runtime·sched
へのロックが追加され、スレッドセーフティが確保されています。
8. runtime·sigprof
関数の変更
runtime·sigprof
は、プロファイリングのためにシグナルが受信された際に呼び出される関数です。
追加された行:
+\tif(m == nil || m->mcache == nil)\n+\t\treturn;\
これは、m
(現在のOSスレッド)またはm->mcache
(Mのキャッシュ)がnil
である場合に、関数を早期に終了させるための防御的なチェックです。プロファイリング中にこれらのポインタが不正な状態である可能性を考慮し、クラッシュを防ぐための堅牢性向上策と考えられます。
これらの変更は、個々には小さいものですが、全体として新しいスケジューラがGoランタイムに統合される際の摩擦を減らし、よりスムーズな移行を可能にするための重要な準備作業であったと言えます。
コアとなるコードの変更箇所
diff --git a/src/pkg/runtime/proc.c b/src/pkg/runtime/proc.c
index e2ba4b6614..f1e3ad59d7 100644
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -239,6 +239,8 @@ runtime·main(void)\n \t// by calling runtime.LockOSThread during initialization\n \t// to preserve the lock.\n \truntime·lockOSThread();\n+\tif(m != &runtime·m0)\n+\t\truntime·throw("runtime·main not on m0");\n \t// From now on, newgoroutines may use non-main threads.\n \tsetmcpumax(runtime·gomaxprocs);\n \truntime·sched.init = true;\
@@ -255,7 +257,7 @@ runtime·main(void)\n \tmain·main();\n \tif(raceenabled)\n \t\truntime·racefini();\n-\t\n+\n \t// Make racy client program work: if panicking on\n \t// another goroutine at the same time as main returns,\n \t// let the other goroutine finish printing the panic trace.\n@@ -669,9 +671,10 @@ int32\n runtime·gcprocs(void)\n {\n \tint32 n;\n-\t\n+\n \t// Figure out how many CPUs to use during GC.\n \t// Limited by gomaxprocs, number of actual CPUs, and MaxGcproc.\n+\truntime·lock(&runtime·sched);\n \tn = runtime·gomaxprocs;\n \tif(n > runtime·ncpu)\n \t\tn = runtime·ncpu;\
@@ -679,9 +682,26 @@ runtime·gcprocs(void)\n \t\tn = MaxGcproc;\n \tif(n > runtime·sched.mwait+1) // one M is currently running\n \t\tn = runtime·sched.mwait+1;\n+\truntime·unlock(&runtime·sched);\n \treturn n;\n }\n \n+static bool\n+needaddgcproc(void)\n+{\\n+\tint32 n;\n+\n+\truntime·lock(&runtime·sched);\n+\tn = runtime·gomaxprocs;\n+\tif(n > runtime·ncpu)\n+\t\tn = runtime·ncpu;\n+\tif(n > MaxGcproc)\n+\t\tn = MaxGcproc;\n+\tn -= runtime·sched.mwait+1; // one M is currently running\n+\truntime·unlock(&runtime·sched);\n+\treturn n > 0;\n+}\n+\n void\n runtime·helpgc(int32 nproc)\n {\n@@ -740,20 +760,14 @@ void\n runtime·starttheworld(void)\n {\n \tM *mp;\n-\tint32 max;\n-\t\n-\t// Figure out how many CPUs GC could possibly use.\n-\tmax = runtime·gomaxprocs;\n-\tif(max > runtime·ncpu)\n-\t\tmax = runtime·ncpu;\n-\tif(max > MaxGcproc)\n-\t\tmax = MaxGcproc;\n+\tbool add;\n \n+\tadd = needaddgcproc();\n \tschedlock();\n \truntime·gcwaiting = 0;\n \tsetmcpumax(runtime·gomaxprocs);\n \tmatchmg();\n-\tif(runtime·gcprocs() < max && canaddmcpu()) {\n+\tif(add && canaddmcpu()) {\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 \t\t// It would have been even better to start it before the collection,\n@@ -866,7 +880,7 @@ runtime·allocm(void)\n \t\tmp->g0 = runtime·malg(-1);\n \telse\n \t\tmp->g0 = runtime·malg(8192);\n-\t\n+\n \treturn mp;\n }\n \n@@ -921,13 +935,13 @@ runtime·needm(byte x)\n \t// Set needextram when we\'ve just emptied the list,\n \t// so that the eventual call into cgocallbackg will\n \t// allocate a new m for the extra list. We delay the\n-\t// allocation until then so that it can be done \n+\t// allocation until then so that it can be done\n \t// after exitsyscall makes sure it is okay to be\n \t// running at all (that is, there\'s no garbage collection\n-\t// running right now).\t\n+\t// running right now).\n \tmp->needextram = mp->schedlink == nil;\n \tunlockextra(mp->schedlink);\n-\t\n+\n \t// Install m and g (= m->g0) and set the stack bounds\n \t// to match the current stack. We don\'t actually know\n \t// how big the stack is, like we don\'t know how big any\n@@ -995,7 +1009,7 @@ runtime·newextram(void)\n // The main expense here is the call to signalstack to release the\n // m\'s signal stack, and then the call to needm on the next callback\n // from this thread. It is tempting to try to save the m for next time,\n-// which would eliminate both these costs, but there might not be \n+// which would eliminate both these costs, but there might not be\n // a next time: the current thread (which Go does not control) might exit.\n // If we saved the m for that thread, there would be an m leak each time\n // such a thread exited. Instead, we acquire and release an m on each\n@@ -1042,7 +1056,7 @@ lockextra(bool nilokay)\n {\n \tM *mp;\n \tvoid (*yield)(void);\n-\t\n+\n \tfor(;;) {\n \t\tmp = runtime·atomicloadp(&runtime·extram);\n \t\tif(mp == MLOCKED) {\n@@ -1077,7 +1091,7 @@ M*\n runtime·newm(void)\n {\n \tM *mp;\n-\t\n+\n \tmp = runtime·allocm();\n \n \tif(runtime·iscgo) {\n@@ -1171,9 +1185,8 @@ schedule(G *gp)\n \tif(m->profilehz != hz)\n \t\truntime·resetcpuprofiler(hz);\n \n-\tif(gp->sched.pc == (byte*)runtime·goexit) {\t// kickoff\n+\tif(gp->sched.pc == (byte*)runtime·goexit) // kickoff\n \t\truntime·gogocallfn(&gp->sched, gp->fnstart);\n-\t}\n \truntime·gogo(&gp->sched, 0);\n }\n \n@@ -1603,7 +1616,7 @@ UnlockOSThread(void)\n \t\treturn;\n \tm->lockedg = nil;\n \tg->lockedm = nil;\n-}\t\n+}\n \n void\n runtime·UnlockOSThread(void)\n@@ -1646,14 +1659,25 @@ runtime·mid(uint32 ret)\n void\n runtime·NumGoroutine(intgo ret)\n {\n-\tret = runtime·sched.gcount;\n+\tret = runtime·gcount();\n \tFLUSH(&ret);\n }\n \n int32\n runtime·gcount(void)\n {\n-\treturn runtime·sched.gcount;\n+\tG *gp;\n+\tint32 n, s;\n+\n+\tn = 0;\n+\truntime·lock(&runtime·sched);\n+\tfor(gp = runtime·allg; gp; gp = gp->alllink) {\n+\t\ts = gp->status;\n+\t\tif(s == Grunnable || s == Grunning || s == Gsyscall || s == Gwaiting)\n+\t\t\tn++;\n+\t}\n+\truntime·unlock(&runtime·sched);\n+\treturn n;\n }\n \n int32\n@@ -1687,6 +1711,8 @@ runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp)\n {\n \tint32 n;\n \n+\tif(m == nil || m->mcache == nil)\n+\t\treturn;\n \tif(prof.fn == nil || prof.hz == 0)\n \t\treturn;\n \n```
## コアとなるコードの解説
### `runtime·main`関数の変更
```c
+\tif(m != &runtime·m0)\n+\t\truntime·throw("runtime·main not on m0");
このコードは、Goプログラムの起動時にruntime·main
関数が、ランタイムが最初に起動するOSスレッドであるruntime·m0
上で実行されていることを確認します。もしm
(現在のOSスレッド)がruntime·m0
と異なる場合、runtime·throw
を呼び出して致命的なエラーを発生させます。これは、ランタイムの初期化が特定のOSスレッドに強く依存しているため、その前提が崩れないようにするための防御的なチェックです。新しいスケジューラがOSスレッドの管理方法を変更する際に、この初期化の制約が維持されることを保証する意図があります。
runtime·gcprocs
関数の変更
+\truntime·lock(&runtime·sched);\n \tn = runtime·gomaxprocs;\n \tif(n > runtime·ncpu)\n \t\tn = runtime·ncpu;\n \tif(n > MaxGcproc)\n \t\tn = MaxGcproc;\n \tif(n > runtime·sched.mwait+1) // one M is currently running\n \t\tn = runtime·sched.mwait+1;\n+\truntime·unlock(&runtime·sched);\
runtime·gcprocs
関数は、ガベージコレクション中に使用するCPUの数を決定します。この変更では、runtime·sched
構造体へのアクセスをruntime·lock
とruntime·unlock
で囲むことで、スレッドセーフティを確保しています。runtime·sched
はスケジューラのグローバルな状態を保持しており、複数のゴルーチンやMから同時にアクセスされる可能性があります。GCプロセスの数を計算する際に、この共有データが競合状態によって不正な値になることを防ぐために、排他制御が導入されました。これは、新しいスケジューラが並行性をより重視する設計になっていることと関連している可能性があります。
needaddgcproc
関数の追加
+static bool\n+needaddgcproc(void)\n+{\n+\tint32 n;\n+\n+\truntime·lock(&runtime·sched);\n+\tn = runtime·gomaxprocs;\n+\tif(n > runtime·ncpu)\n+\t\tn = runtime·ncpu;\n+\tif(n > MaxGcproc)\n+\t\tn = MaxGcproc;\n+\tn -= runtime·sched.mwait+1; // one M is currently running\n+\truntime·unlock(&runtime·sched);\n+\treturn n > 0;\n+}\
この新しい関数は、GCヘルパープロセッサ(M)を追加する必要があるかどうかを判断します。runtime·gcprocs
と同様のロジックで、GOMAXPROCS
、利用可能なCPU数、GCプロセッサの最大数に基づいて、GCに利用できるMの理想的な数を計算します。そして、現在待機中のMの数と比較し、追加のMが必要であればtrue
を返します。この関数は、GCの効率を向上させるために、必要に応じてMを動的に起動するスケジューラのロジックをカプセル化するために導入されました。
runtime·starttheworld
関数の変更
-\tint32 max;\n-\t\n-\t// Figure out how many CPUs GC could possibly use.\n-\tmax = runtime·gomaxprocs;\n-\tif(max > runtime·ncpu)\n-\t\tmax = runtime·ncpu;\n-\tif(max > MaxGcproc)\n-\t\tmax = MaxGcproc;\n+\tbool add;\n \n+\tadd = needaddgcproc();\n \tschedlock();\n \truntime·gcwaiting = 0;\n \tsetmcpumax(runtime·gomaxprocs);\n \tmatchmg();\n-\tif(runtime·gcprocs() < max && canaddmcpu()) {\n+\tif(add && canaddmcpu()) {\
runtime·starttheworld
関数は、GCのStop-the-Worldフェーズが終了し、ゴルーチンの実行が再開される際に呼び出されます。この変更では、GCヘルパープロセッサを追加する必要があるかどうかの判断ロジックが、新しく導入されたneedaddgcproc()
関数に置き換えられました。これにより、コードの重複が排除され、GCプロセッサの管理ロジックが一元化されます。これは、新しいスケジューラがGC中のMの管理をより効率的かつ柔軟に行うための準備です。
runtime·NumGoroutine
とruntime·gcount
関数の変更
-\tret = runtime·sched.gcount;\n+\tret = runtime·gcount();\n \tFLUSH(&ret);\n }\n \n int32\n runtime·gcount(void)\n {\n-\treturn runtime·sched.gcount;\n+\tG *gp;\n+\tint32 n, s;\n+\n+\tn = 0;\n+\truntime·lock(&runtime·sched);\n+\tfor(gp = runtime·allg; gp; gp = gp->alllink) {\n+\t\ts = gp->status;\n+\t\tif(s == Grunnable || s == Grunning || s == Gsyscall || s == Gwaiting)\n+\t\t\tn++;\n+\t}\n+\truntime·unlock(&runtime·sched);\n+\treturn n;\n}\
以前はruntime·NumGoroutine
がruntime·sched.gcount
という静的なカウンタを直接参照してゴルーチン数を取得していましたが、この変更により、新しく実装されたruntime·gcount()
関数を呼び出すようになりました。runtime·gcount()
関数は、runtime·allg
リンクリストを走査し、Grunnable
(実行可能)、Grunning
(実行中)、Gsyscall
(システムコール中)、Gwaiting
(待機中)の状態にあるゴルーチンを動的に数え上げます。この変更は、ゴルーチン数のカウント方法をより正確かつ堅牢にするためのものです。特に、新しいスケジューラがゴルーチンの状態遷移や管理方法を変更する際に、静的なカウンタよりも動的な計算の方が信頼性が高いため、このような変更が行われたと考えられます。また、runtime·sched
へのロックが追加され、スレッドセーフティが確保されています。
runtime·sigprof
関数の変更
+\tif(m == nil || m->mcache == nil)\n+\t\treturn;\
runtime·sigprof
関数は、プロファイリングのためにシグナルが受信された際に呼び出されます。この変更では、m
(現在のOSスレッド)またはm->mcache
(Mのキャッシュ)がnil
である場合に、関数を早期に終了させるための防御的なチェックが追加されました。これは、プロファイリング中にこれらのポインタが不正な状態である可能性を考慮し、ランタイムのクラッシュを防ぐための堅牢性向上策です。
関連リンク
参考にした情報源リンク
- Go Scheduler: https://go.dev/doc/articles/go_scheduler.html
- Go's new scheduler: https://go.dev/blog/go-concurrency-patterns-pipelines (直接的な記事ではないが、当時の並行処理の文脈を理解するのに役立つ)
- Go runtime source code: https://github.com/golang/go/tree/master/src/runtime
- Dmitriy Vyukov's contributions to Go: (一般的な検索で彼のスケジューラ関連の貢献が見つかる)
- GoのM:P:Gモデルに関する解説記事 (多数存在するため、特定のURLは挙げないが、概念理解に利用)
- Goのガベージコレクションに関する解説記事 (多数存在するため、特定のURLは挙げないが、概念理解に利用)
- Goの
proc.c
ファイルに関する議論やドキュメント (当時のGoコミュニティの議論や設計ドキュメントを参照) - Goのコミット履歴と関連するコードレビュー (GitHubの履歴を遡って関連するコミットや議論を確認)