[インデックス 15542] ファイルの概要
このコミットでは、Goランタイムのスケジューラに関連するファイルが変更されています。具体的には、以下の2つのファイルが修正されました。
src/pkg/runtime/proc.c
: Goランタイムのプロセス管理とスケジューリングのコアロジックが含まれるC言語のファイルです。src/pkg/runtime/runtime.h
: Goランタイムの内部で使用されるデータ構造や関数の宣言が含まれるヘッダーファイルです。
コミット
commit 3611553c3b08d615a34581274b553da6c94f193c
Author: Russ Cox <rsc@golang.org>
Date: Fri Mar 1 14:57:05 2013 -0500
runtime: add atomics to fix arm
R=golang-dev, minux.ma
CC=golang-dev
https://golang.org/cl/7429046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3611553c3b08d615a34581274b553da6c94f193c
元コミット内容
runtime: add atomics to fix arm
R=golang-dev, minux.ma
CC=golang-dev
https://golang.org/cl/7429046
変更の背景
このコミットの主な目的は、GoランタイムにおけるARMアーキテクチャでの問題を修正することです。コミットメッセージにある「add atomics to fix arm」という記述から、ARMプロセッサ上でGoプログラムが正しく動作しない、あるいは競合状態(race condition)によって予期せぬ振る舞いを起こす問題があったことが示唆されます。
Goランタイムは、複数のゴルーチン(goroutine)が並行して動作し、M(Machine)、P(Processor)、G(Goroutine)というモデルに基づいてスケジューリングされます。この複雑な並行処理環境では、共有される状態変数へのアクセスが適切に同期されていないと、データ競合が発生し、プログラムのクラッシュや誤った結果につながる可能性があります。特に、ARMのような一部のアーキテクチャでは、メモリモデルがx86などと比較して弱く、コンパイラやCPUによる命令の並べ替え(reordering)がより積極的に行われることがあります。このため、明示的なアトミック操作やメモリバリアを使用しないと、あるCPUコアで行われた変更が別のCPUコアからすぐに認識されない、といった問題が発生しやすくなります。
このコミットは、Goランタイムのスケジューラが使用する共有変数(npidle
, nmspinning
, sysmonwait
, gcwaiting
など)へのアクセスが、ARMアーキテクチャ上でアトミックに保証されていなかったために発生していた問題を解決するために導入されました。これらの変数は、アイドル状態のPの数、スピン中のMの数、システムモニターの待機状態、GCの待機状態など、ランタイムの重要な状態を示すものであり、複数のMやPから同時に読み書きされる可能性があります。非アトミックなアクセスは、これらの変数の値が一時的に不正な状態になったり、古い値が読み取られたりする原因となり、スケジューラの誤動作を引き起こす可能性がありました。
前提知識の解説
1. Goランタイムのスケジューラ (M, P, Gモデル)
Go言語の並行処理は、Goランタイムのスケジューラによって管理されます。このスケジューラは、以下の3つの主要な要素で構成されます。
- G (Goroutine): Go言語における軽量なスレッドです。数千、数万といった単位で生成され、Goランタイムによって管理されます。
- M (Machine): オペレーティングシステム(OS)のスレッドに対応します。Goランタイムは、OSスレッド上でゴルーチンを実行します。
- P (Processor): 論理プロセッサ、またはコンテキストです。MとGの仲介役となり、Gを実行するためのコンテキストを提供します。Pの数は通常、
GOMAXPROCS
環境変数によって制御され、CPUコアの数に設定されることが多いです。
スケジューラは、利用可能なPにGを割り当て、MがそのP上でGを実行します。複数のMが同時に動作し、共有されるPやGのキューにアクセスするため、これらの共有リソースへのアクセスは厳密に同期される必要があります。
2. アトミック操作 (Atomic Operations)
アトミック操作とは、複数のCPUコアやスレッドから同時にアクセスされた場合でも、その操作全体が不可分(atomic)であることを保証する操作です。つまり、操作の途中で他のスレッドから割り込まれることがなく、常に一貫性のある結果が得られます。
並行プログラミングにおいて、共有変数への読み書きは、アトミック操作を使用しないとデータ競合を引き起こす可能性があります。例えば、あるスレッドが変数を更新している最中に、別のスレッドがその変数を読み取ると、部分的に更新された不正な値が読み取られる可能性があります。アトミック操作は、このような問題を回避するために、ハードウェアレベルでのサポート(例: ロックフリーな命令)や、より高レベルな同期プリミティブ(ミューテックスなど)を用いて実現されます。
Go言語では、sync/atomic
パッケージを通じてアトミック操作が提供されていますが、ランタイム内部ではより低レベルなアトミック操作が直接使用されます。
3. メモリモデルとメモリバリア
メモリモデルは、複数のCPUコアが共有メモリにアクセスする際の振る舞いを定義するものです。CPUやコンパイラは、パフォーマンス向上のために命令の実行順序を並べ替えたり、キャッシュを利用したりすることがあります。これにより、あるCPUコアで行われたメモリへの書き込みが、別のCPUコアからすぐに認識されない、あるいは異なる順序で認識される可能性があります。
メモリバリア(Memory Barrier)またはメモリフェンス(Memory Fence)は、このような命令の並べ替えやキャッシュの可視性に関する問題を制御するための命令です。メモリバリアを挿入することで、特定のメモリ操作が完了するまで、それ以降のメモリ操作が開始されないことを保証したり、特定のメモリ操作が他のCPUコアから可視になることを保証したりできます。アトミック操作は、通常、適切なメモリバリアを含んでおり、可視性と順序付けの問題を解決します。
ARMアーキテクチャは、x86と比較して弱いメモリモデルを持つことで知られています。これは、x86が比較的強い順序付けをデフォルトで提供するのに対し、ARMではより多くの並べ替えが許容されるため、明示的な同期プリミティブ(アトミック操作やメモリバリア)の使用がより重要になることを意味します。
技術的詳細
このコミットの技術的な核心は、Goランタイムのスケジューラが使用するいくつかの共有変数へのアクセスを、非アトミックな操作からアトミックな操作に置き換えることです。
具体的には、以下の変更が行われています。
-
変数の型変更:
runtime·sched.sysmonwait
:bool
型からuint32
型に変更されました。runtime·gcwaiting
:int32
型からuint32
型に変更されました。 これらの変数は、アトミック操作の対象となるため、通常は整数型(uint32
やint32
)が使用されます。bool
型を直接アトミックに操作するのではなく、0
をfalse
、1
をtrue
としてuint32
で表現することで、アトミックな読み書きが可能になります。
-
アトミック操作関数の導入:
runtime·atomicload(addr)
: 指定されたアドレスaddr
から値をアトミックに読み取ります。runtime·atomicstore(addr, val)
: 指定されたアドレスaddr
にval
をアトミックに書き込みます。runtime·xadd(addr, delta)
: 指定されたアドレスaddr
の値をdelta
だけアトミックに加算(または減算)し、その結果の値を返します。これは「fetch-and-add」操作として知られています。runtime·cas(addr, old, new)
: Compare-And-Swap(CAS)操作です。指定されたアドレスaddr
の値がold
と等しい場合のみ、その値をnew
にアトミックに更新します。更新が成功した場合はtrue
、失敗した場合はfalse
を返します。
これらのアトミック操作関数は、Goランタイムの内部で定義されており、各アーキテクチャ(ARM、x86など)の特性に合わせて最適化されたアセンブリ命令やコンパイラ組み込み関数(intrinsics)を利用して実装されています。これにより、低レベルでの効率的な同期が実現されます。
変更された箇所では、runtime·sched.npidle
(アイドル状態のPの数)、runtime·sched.nmspinning
(スピン中のMの数)、runtime·sched.sysmonwait
(システムモニターの待機状態)、runtime·gcwaiting
(GCの待機状態)といった、スケジューラの重要な状態変数へのアクセスが、これらのアトミック操作関数を通じて行われるようになりました。これにより、複数のゴルーチンやMがこれらの変数に同時にアクセスしても、データ競合が発生せず、常に正しい状態が読み書きされることが保証されます。
特に、runtime·sched.npidle
やruntime·sched.nmspinning
のようなカウンタ変数は、runtime·xadd
を使用してアトミックに増減されることで、正確なカウントが維持されます。また、runtime·sched.sysmonwait
やruntime·gcwaiting
のようなフラグ変数は、runtime·atomicload
とruntime·atomicstore
を使用してアトミックに読み書きされることで、状態の遷移が正しく同期されます。
コアとなるコードの変更箇所
このコミットで変更された主要なコード箇所は以下の通りです。
src/pkg/runtime/proc.c
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -45,7 +45,7 @@ struct Sched {
int32 stopwait;
Note stopnote;
- bool sysmonwait;
+ uint32 sysmonwait;
Note sysmonnote;
int32 profilehz; // cpu profiling rate
@@ -59,7 +59,7 @@ Sched runtime·sched;
int32 runtime·gomaxprocs;
bool runtime·singleproc;
bool runtime·iscgo;
-int32 runtime·gcwaiting;
+uint32 runtime·gcwaiting;
M runtime·m0;
G runtime·g0; // idle goroutine for m0
G* runtime·allg;
@@ -277,7 +277,7 @@ runtime·ready(G *gp)
}
gp->status = Grunnable;
runqput(m->p, gp);
- if(runtime·sched.npidle != 0 && runtime·sched.nmspinning == 0) // TODO: fast atomic
+ if(runtime·atomicload(&runtime·sched.npidle) != 0 && runtime·atomicload(&runtime·sched.nmspinning) == 0) // TODO: fast atomic
wakep();
}
@@ -842,7 +842,7 @@ handoffp(P *p)
}
// no local work, check that there are no spinning/idle M's,
// otherwise our help is not required
- if(runtime·sched.nmspinning + runtime·sched.npidle == 0 && // TODO: fast atomic
+ if(runtime·atomicload(&runtime·sched.nmspinning) + runtime·atomicload(&runtime·sched.npidle) == 0 && // TODO: fast atomic
runtime·cas(&runtime·sched.nmspinning, 0, 1)) {
startm(p, true);
return;
@@ -996,7 +996,7 @@ top:
// If number of spinning M's >= number of busy P's, block.
// This is necessary to prevent excessive CPU consumption
// when GOMAXPROCS>>1 but the program parallelism is low.
- if(!m->spinning && 2 * runtime·sched.nmspinning >= runtime·gomaxprocs - runtime·sched.npidle) // TODO: fast atomic
+ if(!m->spinning && 2 * runtime·atomicload(&runtime·sched.nmspinning) >= runtime·gomaxprocs - runtime·atomicload(&runtime·sched.npidle)) // TODO: fast atomic
goto stop;
if(!m->spinning) {
m->spinning = true;
@@ -1079,8 +1079,8 @@ top:
// M wakeup policy is deliberately somewhat conservative (see nmspinning handling),
// so see if we need to wakeup another M here.
if (m->p->runqhead != m->p->runqtail &&
- runtime·sched.nmspinning == 0 &&
- runtime·sched.npidle > 0) // TODO: fast atomic
+ runtime·atomicload(&runtime·sched.nmspinning) == 0 &&
+ runtime·atomicload(&runtime·sched.npidle) > 0) // TODO: fast atomic
wakep();
if(gp->lockedm) {
@@ -1197,10 +1197,10 @@ void
runtime·throw("entersyscall");
}
- if(runtime·sched.sysmonwait) { // TODO: fast atomic
+ if(runtime·atomicload(&runtime·sched.sysmonwait)) { // TODO: fast atomic
runtime·lock(&runtime·sched);
- if(runtime·sched.sysmonwait) {
- runtime·sched.sysmonwait = false;
+ if(runtime·atomicload(&runtime·sched.sysmonwait)) {
+ runtime·atomicstore(&runtime·sched.sysmonwait, 0);
runtime·notewakeup(&runtime·sched.sysmonnote);
}
runtime·unlock(&runtime·sched);
@@ -1457,7 +1457,7 @@ runtime·newproc1(FuncVal *fn, byte *argp, int32 narg, int32 nret, void *callerp
newg->racectx = runtime·racegostart(callerpc);
runqput(m->p, newg);
- if(runtime·sched.npidle != 0 && runtime·sched.nmspinning == 0 && fn->fn != runtime·main) // TODO: fast atomic
+ if(runtime·atomicload(&runtime·sched.npidle) != 0 && runtime·atomicload(&runtime·sched.nmspinning) == 0 && fn->fn != runtime·main) // TODO: fast atomic
wakep();
return newg;
}
@@ -1915,10 +1915,10 @@ sysmon(void)
if(delay > 10*1000) // up to 10ms
delay = 10*1000;
runtime·usleep(delay);
- if(runtime·gcwaiting || runtime·sched.npidle == runtime·gomaxprocs) { // TODO: fast atomic
+ if(runtime·atomicload(&runtime·gcwaiting) || runtime·atomicload(&runtime·sched.npidle) == runtime·gomaxprocs) { // TODO: fast atomic
runtime·lock(&runtime·sched);
- if(runtime·gcwaiting || runtime·sched.npidle == runtime·gomaxprocs) {
- runtime·sched.sysmonwait = true;
+ if(runtime·atomicload(&runtime·gcwaiting) || runtime·atomicload(&runtime·sched.npidle) == runtime·gomaxprocs) {
+ runtime·atomicstore(&runtime·sched.sysmonwait, 1);
runtime·unlock(&runtime·sched);
runtime·notesleep(&runtime·sched.sysmonnote);
runtime·noteclear(&runtime·sched.sysmonnote);
@@ -1954,7 +1954,7 @@ retake(uint32 *ticks)
s = p->status;
if(s != Psyscall)
continue;
- if(p->runqhead == p->runqtail && runtime·sched.nmspinning + runtime·sched.npidle > 0) // TODO: fast atomic
+ if(p->runqhead == p->runqtail && runtime·atomicload(&runtime·sched.nmspinning) + runtime·atomicload(&runtime·sched.npidle) > 0) // TODO: fast atomic
continue;
// Need to increment number of locked M's before the CAS.
// Otherwise the M from which we retake can exit the syscall,
@@ -2042,7 +2042,7 @@ pidleput(P *p)
{
p->link = runtime·sched.pidle;
runtime·sched.pidle = p;
- runtime·sched.npidle++; // TODO: fast atomic
+ runtime·xadd(&runtime·sched.npidle, 1); // TODO: fast atomic
}
// Try get a p from pidle list.
@@ -2055,7 +2555,7 @@ pidleget(void)
p = runtime·sched.pidle;
if(p) {
runtime·sched.pidle = p->link;
- runtime·sched.npidle--; // TODO: fast atomic
+ runtime·xadd(&runtime·sched.npidle, -1); // TODO: fast atomic
}
return p;
}
src/pkg/runtime/runtime.h
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -628,7 +628,7 @@ extern P** runtime·allp;
extern int32 runtime·gomaxprocs;
extern bool runtime·singleproc;
extern uint32 runtime·panicking;
-extern int32 runtime·gcwaiting; // gc is waiting to run
+extern uint32 runtime·gcwaiting; // gc is waiting to run
extern int8* runtime·goos;
extern int32 runtime·ncpu;
extern bool runtime·iscgo;
コアとなるコードの解説
このコミットの主要な変更は、Goランタイムのスケジューラが使用する共有変数へのアクセス方法を、非アトミックな直接アクセスからアトミックな操作に切り替えた点にあります。
-
sysmonwait
とgcwaiting
の型変更:src/pkg/runtime/proc.c
とsrc/pkg/runtime/runtime.h
で、runtime·sched.sysmonwait
の型がbool
からuint32
に、runtime·gcwaiting
の型がint32
からuint32
に変更されました。- これは、アトミック操作が通常、整数型に対して定義されるためです。
bool
値は0
(false)または1
(true)としてuint32
で表現されます。これにより、これらのフラグ変数をアトミックに読み書きできるようになります。
-
runtime·atomicload
の導入:runtime·sched.npidle
、runtime·sched.nmspinning
、runtime·sched.sysmonwait
、runtime·gcwaiting
といった変数の読み取り箇所が、runtime·atomicload(&variable_name)
に置き換えられました。- 例:
if(runtime·sched.npidle != 0 ...)
がif(runtime·atomicload(&runtime·sched.npidle) != 0 ...)
に変更。 - これにより、これらの変数の値が、他のMやPによる並行な書き込みの影響を受けずに、常に最新かつ一貫性のある状態で読み取られることが保証されます。特に、ARMのような弱いメモリモデルを持つアーキテクチャでは、この明示的なアトミックロードが重要になります。
-
runtime·atomicstore
の導入:runtime·sched.sysmonwait
への書き込み箇所が、runtime·sched.sysmonwait = false;
からruntime·atomicstore(&runtime·sched.sysmonwait, 0);
に変更されました。- 同様に、
runtime·sched.sysmonwait = true;
からruntime·atomicstore(&runtime·sched.sysmonwait, 1);
に変更されました。 - これにより、
sysmonwait
フラグの状態変更がアトミックに行われ、他のMやPから即座に可視になることが保証されます。
-
runtime·xadd
の導入:pidleput
関数内のruntime·sched.npidle++;
がruntime·xadd(&runtime·sched.npidle, 1);
に変更されました。pidleget
関数内のruntime·sched.npidle--;
がruntime·xadd(&runtime·sched.npidle, -1);
に変更されました。runtime·xadd
は、変数の値をアトミックに増減させる操作です。これにより、複数のMやPが同時にnpidle
(アイドル状態のPの数)を増減させても、カウンタの整合性が保たれ、正確なPの数が維持されます。これは、並行環境における共有カウンタの更新に不可欠な操作です。
-
runtime·cas
の継続使用:handoffp
関数では、既存のruntime·cas(&runtime·sched.nmspinning, 0, 1)
が引き続き使用されています。CAS(Compare-And-Swap)は、特定の条件(現在の値が期待値と一致すること)が満たされた場合にのみ値を更新するアトミック操作であり、ロックフリーなアルゴリズムの実装によく用いられます。このコミットでは、CASの既存の利用箇所は変更されていませんが、他のアトミック操作の導入と合わせて、ランタイム全体の同期メカニズムが強化されています。
これらの変更により、Goランタイムのスケジューラが管理する重要な共有状態変数が、データ競合のリスクなしに、複数の並行実行エンティティ(MやP)から安全にアクセスできるようになりました。特にARMアーキテクチャのような弱いメモリモデルを持つ環境では、このような明示的なアトミック操作の導入が、プログラムの安定性と正確性を確保するために不可欠です。
関連リンク
- Go CL 7429046: https://golang.org/cl/7429046
参考にした情報源リンク
- Go言語のスケジューラについて:
- アトミック操作とメモリモデルについて:
- アトミック操作とは - IT用語辞典 e-Words
- メモリモデルとメモリバリア - Qiita
- ARMのメモリモデルと同期プリミティブ - Qiita (一般的なARMメモリモデルの解説)
- Goの
sync/atomic
パッケージ: