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

[インデックス 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·mainm0以外の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と同様のロジックで、gomaxprocsncpuMaxGcprocに基づいて必要な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·NumGoroutineruntime·gcount関数の変更

runtime·NumGoroutineは、現在実行中のゴルーチンの数を返す関数です。 変更点:

  • runtime·NumGoroutineruntime·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リンクリストを走査し、GrunnableGrunningGsyscallGwaitingの状態にあるゴルーチンを数えることで、動的にゴルーチン数を計算します。この変更は、ゴルーチン数のカウント方法をより正確かつ堅牢にするためのものです。特に、新しいスケジューラがゴルーチンの状態遷移や管理方法を変更する際に、静的なカウンタよりも動的な計算の方が信頼性が高いため、このような変更が行われたと考えられます。また、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·lockruntime·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·NumGoroutineruntime·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·NumGoroutineruntime·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·mainm0以外の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と同様のロジックで、gomaxprocsncpuMaxGcprocに基づいて必要な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·NumGoroutineruntime·gcount関数の変更

runtime·NumGoroutineは、現在実行中のゴルーチンの数を返す関数です。 変更点:

  • runtime·NumGoroutineruntime·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リンクリストを走査し、GrunnableGrunningGsyscallGwaitingの状態にあるゴルーチンを数えることで、動的にゴルーチン数を計算します。この変更は、ゴルーチン数のカウント方法をより正確かつ堅牢にするためのものです。特に、新しいスケジューラがゴルーチンの状態遷移や管理方法を変更する際に、静的なカウンタよりも動的な計算の方が信頼性が高いため、このような変更が行われたと考えられます。また、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·lockruntime·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·NumGoroutineruntime·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·NumGoroutineruntime·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の履歴を遡って関連するコミットや議論を確認)