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

[インデックス 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ランタイムのスケジューラが使用するいくつかの共有変数へのアクセスを、非アトミックな操作からアトミックな操作に置き換えることです。

具体的には、以下の変更が行われています。

  1. 変数の型変更:

    • runtime·sched.sysmonwait: bool型からuint32型に変更されました。
    • runtime·gcwaiting: int32型からuint32型に変更されました。 これらの変数は、アトミック操作の対象となるため、通常は整数型(uint32int32)が使用されます。bool型を直接アトミックに操作するのではなく、0false1trueとしてuint32で表現することで、アトミックな読み書きが可能になります。
  2. アトミック操作関数の導入:

    • runtime·atomicload(addr): 指定されたアドレスaddrから値をアトミックに読み取ります。
    • runtime·atomicstore(addr, val): 指定されたアドレスaddrvalをアトミックに書き込みます。
    • 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.npidleruntime·sched.nmspinningのようなカウンタ変数は、runtime·xaddを使用してアトミックに増減されることで、正確なカウントが維持されます。また、runtime·sched.sysmonwaitruntime·gcwaitingのようなフラグ変数は、runtime·atomicloadruntime·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ランタイムのスケジューラが使用する共有変数へのアクセス方法を、非アトミックな直接アクセスからアトミックな操作に切り替えた点にあります。

  1. sysmonwaitgcwaiting の型変更:

    • src/pkg/runtime/proc.csrc/pkg/runtime/runtime.h で、runtime·sched.sysmonwait の型が bool から uint32 に、runtime·gcwaiting の型が int32 から uint32 に変更されました。
    • これは、アトミック操作が通常、整数型に対して定義されるためです。bool値は0(false)または1(true)としてuint32で表現されます。これにより、これらのフラグ変数をアトミックに読み書きできるようになります。
  2. runtime·atomicload の導入:

    • runtime·sched.npidleruntime·sched.nmspinningruntime·sched.sysmonwaitruntime·gcwaitingといった変数の読み取り箇所が、runtime·atomicload(&variable_name)に置き換えられました。
    • 例: if(runtime·sched.npidle != 0 ...)if(runtime·atomicload(&runtime·sched.npidle) != 0 ...) に変更。
    • これにより、これらの変数の値が、他のMやPによる並行な書き込みの影響を受けずに、常に最新かつ一貫性のある状態で読み取られることが保証されます。特に、ARMのような弱いメモリモデルを持つアーキテクチャでは、この明示的なアトミックロードが重要になります。
  3. 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から即座に可視になることが保証されます。
  4. 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の数が維持されます。これは、並行環境における共有カウンタの更新に不可欠な操作です。
  5. runtime·cas の継続使用:

    • handoffp関数では、既存のruntime·cas(&runtime·sched.nmspinning, 0, 1)が引き続き使用されています。CAS(Compare-And-Swap)は、特定の条件(現在の値が期待値と一致すること)が満たされた場合にのみ値を更新するアトミック操作であり、ロックフリーなアルゴリズムの実装によく用いられます。このコミットでは、CASの既存の利用箇所は変更されていませんが、他のアトミック操作の導入と合わせて、ランタイム全体の同期メカニズムが強化されています。

これらの変更により、Goランタイムのスケジューラが管理する重要な共有状態変数が、データ競合のリスクなしに、複数の並行実行エンティティ(MやP)から安全にアクセスできるようになりました。特にARMアーキテクチャのような弱いメモリモデルを持つ環境では、このような明示的なアトミック操作の導入が、プログラムの安定性と正確性を確保するために不可欠です。

関連リンク

参考にした情報源リンク