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

[インデックス 14219] ファイルの概要

このコミットは、GoランタイムにおけるゴルーチンIDのデータ型を32ビット整数から64ビット整数に拡張するものです。これにより、システムが生成できるゴルーチンの総数が増加し、長期間稼働するアプリケーションや非常に多くのゴルーチンを生成するアプリケーションにおけるIDの枯渇(オーバーフロー)問題が解決されます。

コミット

commit 320df44f04928285fa55a20d07864d366052b823
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Fri Oct 26 10:13:06 2012 +0400

    runtime: switch to 64-bit goroutine ids
    Fixes #4275.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6759053

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/320df44f04928285fa55a20d07864d366052b823

元コミット内容

diff --git a/src/pkg/runtime/mgc0.c b/src/pkg/runtime/mgc0.c
index dc3b877c4e..4d857bf0b7 100644
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -567,7 +567,7 @@ addstackroots(G *gp)
 	n = 0;
 	while(stk) {
 		if(sp < guard-StackGuard || (byte*)stk < sp) {
-			runtime·printf("scanstack inconsistent: g%d#%d sp=%p not in [%p,%p]\\n", gp->goid, n, sp, guard-StackGuard, stk);
+			runtime·printf("scanstack inconsistent: g%D#%d sp=%p not in [%p,%p]\\n", gp->goid, n, sp, guard-StackGuard, stk);
 			runtime·throw("scanstack");
 		}
 		addroot(sp, (byte*)stk - sp);
diff --git a/src/pkg/runtime/proc.c b/src/pkg/runtime/proc.c
index 5fecf05589..9da748f2f6 100644
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -55,7 +55,7 @@ struct Sched {
 	Lock;
 
 	G *gfree;	// available g's (status == Gdead)
-	int32 goidgen;
+	int64 goidgen;
 
 	G *ghead;	// g's waiting to run
 	G *gtail;
@@ -311,7 +311,7 @@ runtime·goroutineheader(G *gp)
 		status = "???";
 		break;
 	}
-	runtime·printf("goroutine %d [%s]:\\n", gp->goid, status);
+	runtime·printf("goroutine %D [%s]:\\n", gp->goid, status);
 }
 
 void
@@ -391,7 +391,7 @@ gput(G *gp)
 	// If g is the idle goroutine for an m, hand it off.
 	if(gp->idlem != nil) {
 		if(gp->idlem->idleg != nil) {
-			runtime·printf("m%d idle out of sync: g%d g%d\\n",
+			runtime·printf("m%d idle out of sync: g%D g%D\\n",
 				gp->idlem->id,
 				gp->idlem->idleg->goid, gp->goid);
 			runtime·throw("runtime: double idle");
@@ -493,7 +493,7 @@ readylocked(G *gp)
 
 	// Mark runnable.
 	if(gp->status == Grunnable || gp->status == Grunning) {
-		runtime·printf("goroutine %d has status %d\\n", gp->goid, gp->status);
+		runtime·printf("goroutine %D has status %d\\n", gp->goid, gp->status);
 		runtime·throw("bad g->status in ready");
 	}
 	gp->status = Grunnable;
@@ -1100,7 +1100,7 @@ runtime·oldstack(void)
 	uintptr cret;
 	byte *sp;
 	G *g1;
-	int32 goid;
+	int64 goid;
 
 //printf("oldstack m->cret=%p\\n", m->cret);\n
 
@@ -1294,7 +1294,7 @@ runtime·newproc1(byte *fn, byte *argp, int32 narg, int32 nret, void *callerpc)
 	byte *sp;
 	G *newg;
 	int32 siz;
-	int32 goid;
+	int64 goid;
 
 //printf("newproc1 %p %p narg=%d nret=%d\\n", fn, argp, narg, nret);\n
 	siz = narg + nret;
@@ -1307,7 +1307,7 @@ runtime·newproc1(byte *fn, byte *argp, int32 narg, int32 nret, void *callerpc)
 	if(siz > StackMin - 1024)
 		runtime·throw("runtime.newproc: function arguments too large for new goroutine");
 
-	goid = runtime·xadd((uint32*)&runtime·sched.goidgen, 1);
+	goid = runtime·xadd64((uint64*)&runtime·sched.goidgen, 1);
 	if(raceenabled)
 		runtime·racegostart(goid, callerpc);
 
diff --git a/src/pkg/runtime/runtime.h b/src/pkg/runtime/runtime.h
index 83757ba8a3..cd2f6f0587 100644
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -203,7 +203,7 @@ struct	G
 	G*	alllink;	// on allg
 	void*	param;		// passed parameter on wakeup
 	int16	status;
-	int32	goid;
+	int64	goid;
 	uint32	selgen;		// valid sudog pointer
 	int8*	waitreason;	// if status==Gwaiting
 	G*	schedlink;
diff --git a/src/pkg/runtime/traceback_arm.c b/src/pkg/runtime/traceback_arm.c
index 9ca54784ff..c92feb6ed8 100644
--- a/src/pkg/runtime/traceback_arm.c
+++ b/src/pkg/runtime/traceback_arm.c
@@ -147,7 +147,7 @@ runtime·gentraceback(byte *pc0, byte *sp, byte *lr0, G *g, int32 skip, uintptr
 		waspanic = f->entry == (uintptr)runtime·sigpanic;
 
 		if(pcbuf == nil && f->entry == (uintptr)runtime·newstack && g == m->g0) {
-			runtime·printf("----- newstack called from goroutine %d -----\\n", m->curg->goid);
+			runtime·printf("----- newstack called from goroutine %D -----\\n", m->curg->goid);
 			pc = (uintptr)m->morepc;
 			sp = (byte*)m->moreargp - sizeof(void*);
 			lr = (uintptr)m->morebuf.pc;
@@ -158,7 +158,7 @@ runtime·gentraceback(byte *pc0, byte *sp, byte *lr0, G *g, int32 skip, uintptr
 		}
 		
 		if(pcbuf == nil && f->entry == (uintptr)runtime·lessstack && g == m->g0) {
-			runtime·printf("----- lessstack called from goroutine %d -----\\n", m->curg->goid);
+			runtime·printf("----- lessstack called from goroutine %D -----\\n", m->curg->goid);
 			g = m->curg;
 			stk = (Stktop*)g->stackbase;
 			sp = (byte*)stk->gobuf.sp;
diff --git a/src/pkg/runtime/traceback_x86.c b/src/pkg/runtime/traceback_x86.c
index 5a307de3b5..7f53d1136b 100644
--- a/src/pkg/runtime/traceback_x86.c
+++ b/src/pkg/runtime/traceback_x86.c
@@ -165,7 +165,7 @@ runtime·gentraceback(byte *pc0, byte *sp, byte *lr0, G *g, int32 skip, uintptr
 			// The fact that we saw newstack means that morestack
 			// has managed to record its information in m, so we can
 			// use it to keep unwinding the stack.
-			runtime·printf("----- morestack called from goroutine %d -----\\n", m->curg->goid);
+			runtime·printf("----- morestack called from goroutine %D -----\\n", m->curg->goid);
 			pc = (uintptr)m->morepc;
 			sp = (byte*)m->morebuf.sp - sizeof(void*);
 			lr = (uintptr)m->morebuf.pc;
@@ -178,7 +178,7 @@ runtime·gentraceback(byte *pc0, byte *sp, byte *lr0, G *g, int32 skip, uintptr
 
 		if(pcbuf == nil && f->entry == (uintptr)runtime·lessstack && g == m->g0) {
 			// Lessstack is running on scheduler stack.  Switch to original goroutine.
-			runtime·printf("----- lessstack called from goroutine %d -----\\n", m->curg->goid);
+			runtime·printf("----- lessstack called from goroutine %D -----\\n", m->curg->goid);
 			g = m->curg;
 			stk = (Stktop*)g->stackbase;
 			sp = (byte*)stk->gobuf.sp;

変更の背景

このコミットの背景には、Goランタイムがゴルーチンに割り当てる一意の識別子(ID)が32ビット整数であることによる潜在的な問題がありました。32ビット符号付き整数の最大値は約20億(2^31 - 1)です。Goアプリケーションが非常に多くのゴルーチンを生成し、それらが短期間で生成・終了を繰り返すようなシナリオでは、このIDが枯渇し、同じIDが再利用される可能性がありました。

特に、GoのIssue #4275("runtime: goroutine ids can wrap around")でこの問題が報告されました。このIssueでは、ゴルーチンIDが32ビットの範囲を超えてラップアラウンド(循環)する可能性が指摘されており、これによりデバッグやプロファイリングにおいて混乱が生じる可能性がありました。例えば、あるゴルーチンが終了し、そのIDが再利用された場合、ログやトレースで同じIDを持つ異なるゴルーチンが混同される恐れがあります。

この変更は、将来的なGoアプリケーションのスケーラビリティと堅牢性を確保するために不可欠でした。64ビット整数に拡張することで、ゴルーチンIDの枯渇は事実上不可能となり、IDの一意性が保証され、より大規模で長期間稼働するシステムでの安定性が向上します。

前提知識の解説

Goランタイムの基本(Goroutine, M, P, Gスケジューラ)

Go言語は、並行処理を容易にするために「ゴルーチン(Goroutine)」という軽量なスレッドのような抽象化を提供します。ゴルーチンはGoランタイムによって管理され、OSのスレッド(M: Machine)に多重化されて実行されます。この多重化は、Goスケジューラによって行われます。

  • G (Goroutine): Go言語の並行処理の単位。非常に軽量で、数百万個のゴルーチンを同時に実行することも可能です。各ゴルーチンには一意のIDが割り当てられます。
  • M (Machine): OSのスレッドに対応します。Goランタイムは、M上でGを実行します。
  • P (Processor): 論理プロセッサ。MとGの間に位置し、MがGを実行するためのコンテキストを提供します。Pは実行可能なGのキューを保持し、MはPからGを取得して実行します。

Goスケジューラは、G、M、Pの3つの要素を協調させて、効率的な並行処理を実現します。

Goroutine IDの役割

各ゴルーチンには、Goランタイムによって一意の数値IDが割り当てられます。このIDは、主に以下の目的で使用されます。

  • 識別: 実行中の特定のゴルーチンを識別するため。デバッグログやプロファイリングツールでゴルーチンを追跡する際に不可欠です。
  • デバッグ: 問題発生時にどのゴルーチンが関与していたかを特定するのに役立ちます。
  • トレース: システムの動作を分析する際に、ゴルーチンのライフサイクルや相互作用を追跡するために使用されます。

32-bit vs 64-bit整数の違いとオーバーフローの概念

  • 32ビット整数: 32ビット(4バイト)のメモリを使用して数値を表現します。符号付きの場合、約 -2,147,483,648 から 2,147,483,647 までの値を表現できます。
  • 64ビット整数: 64ビット(8バイト)のメモリを使用して数値を表現します。符号付きの場合、約 -9,223,372,036,854,775,808 から 9,223,372,036,854,775,807 までの値を表現できます。

オーバーフロー: 変数が表現できる最大値を超えた場合に発生する現象です。例えば、32ビット符号付き整数が2,147,483,647に達した後、さらに1を加えると、最小値である-2,147,483,648に戻ってしまいます(ラップアラウンド)。ゴルーチンIDの場合、IDが循環してしまうと、過去に存在したゴルーチンと同じIDが新しいゴルーチンに割り当てられることになり、一意性が失われます。

アトミック操作 (xadd, xadd64)

アトミック操作とは、複数のCPUコアやスレッドから同時にアクセスされた場合でも、その操作全体が不可分(アトミック)に実行されることを保証する操作です。これにより、競合状態(race condition)を防ぎ、データの整合性を保ちます。

  • xadd: "exchange and add" の略で、指定されたメモリ位置の値に指定された値を加算し、その結果をメモリに書き込み、元の値を返すアトミック操作です。このコミットでは、32ビット整数に対するアトミック加算 (runtime·xadd) から、64ビット整数に対するアトミック加算 (runtime·xadd64) へと変更されています。これは、ゴルーチンIDの生成が複数のゴルーチンから同時に行われる可能性があるため、IDの重複や不正な生成を防ぐために不可欠です。

C言語とGoの連携(src/pkg/runtimeがC言語で書かれていること)

Go言語の初期のランタイム(src/pkg/runtimeディレクトリ内のコード)は、パフォーマンスと低レベルなシステム制御のためにC言語で書かれていました。これは、Go言語自体がまだ成熟していなかった時期に、OSとのインタラクションやメモリ管理、スケジューリングといったクリティカルな部分を効率的に実装するための方針でした。

このコミットで変更されているファイル群(.c拡張子を持つファイル)は、まさにこのC言語で書かれたランタイムの一部です。Go言語の進化とともに、ランタイムの多くの部分はGo言語自体で書き直されていきましたが、このコミットが作成された2012年時点では、まだC言語のコードが多く残っていました。

技術的詳細

このコミットの主要な目的は、Goランタイムがゴルーチンに割り当てるIDのデータ型を32ビットから64ビットに拡張することです。これにより、ゴルーチンIDの枯渇問題が解決され、より大規模なアプリケーションでの安定性が向上します。

具体的な変更点は以下の通りです。

  1. goidgenの型変更:

    • src/pkg/runtime/proc.c内のstruct Schedにおいて、ゴルーチンIDを生成するためのカウンタであるgoidgenの型がint32からint64に変更されました。
      // proc.c
      -	int32 goidgen;
      +	int64 goidgen;
      
    • goidgenは、新しいゴルーチンが作成されるたびにインクリメントされ、その値が新しいゴルーチンのIDとして割り当てられます。このカウンタが64ビットになることで、生成可能なゴルーチンIDの最大値が大幅に増加します。
  2. goidの型変更:

    • src/pkg/runtime/runtime.h内のstruct G(ゴルーチンを表す構造体)において、各ゴルーチンが持つIDであるgoidの型がint32からint64に変更されました。
      // runtime.h
      -	int32	goid;
      +	int64	goid;
      
    • これにより、個々のゴルーチンが保持するIDも64ビットの値を格納できるようになります。
  3. アトミック操作の変更:

    • src/pkg/runtime/proc.c内のruntime·newproc1関数(新しいゴルーチンを作成する内部関数)において、goidgenをインクリメントするアトミック操作がruntime·xaddからruntime·xadd64に変更されました。
      // proc.c
      -	goid = runtime·xadd((uint32*)&runtime·sched.goidgen, 1);
      +	goid = runtime·xadd64((uint64*)&runtime·sched.goidgen, 1);
      
    • runtime·xaddは32ビット整数に対するアトミック加算、runtime·xadd64は64ビット整数に対するアトミック加算です。goidgenが64ビットになったため、それに合わせて適切なアトミック操作関数が使用されるようになりました。これにより、複数のゴルーチンが同時にIDを要求しても、IDの生成が安全かつ正確に行われることが保証されます。
  4. runtime·printfのフォーマット指定子変更:

    • src/pkg/runtime/mgc0.c, src/pkg/runtime/proc.c, src/pkg/runtime/traceback_arm.c, src/pkg/runtime/traceback_x86.cの各ファイルで、ゴルーチンIDを出力するruntime·printfのフォーマット指定子が%dから%Dに変更されました。
      // 例: mgc0.c
      -			runtime·printf("scanstack inconsistent: g%d#%d sp=%p not in [%p,%p]\\n", gp->goid, n, sp, guard-StackGuard, stk);
      +			runtime·printf("scanstack inconsistent: g%D#%d sp=%p not in [%p,%p]\\n", gp->goid, n, sp, guard-StackGuard, stk);
      
    • Goランタイムの内部で使用されるruntime·printfは、標準Cライブラリのprintfとは異なる独自のフォーマット指定子を持つことがあります。この場合、%Dは64ビット整数(int64)を出力するための指定子として導入されたと考えられます。これにより、64ビットに拡張されたゴルーチンIDが正しくログに出力されるようになります。

これらの変更は、Goランタイムのコア部分に影響を与え、ゴルーチンIDの管理方法を根本的に改善するものです。

コアとなるコードの変更箇所

src/pkg/runtime/proc.c

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -55,7 +55,7 @@ struct Sched {
 	Lock;
 
 	G *gfree;	// available g's (status == Gdead)
-	int32 goidgen;
+	int64 goidgen;
 
 	G *ghead;	// g's waiting to run
 	G *gtail;
@@ -1307,7 +1307,7 @@ runtime·newproc1(byte *fn, byte *argp, int32 narg, int32 nret, void *callerpc)
 	if(siz > StackMin - 1024)
 		runtime·throw("runtime.newproc: function arguments too large for new goroutine");
 
-	goid = runtime·xadd((uint32*)&runtime·sched.goidgen, 1);
+	goid = runtime·xadd64((uint64*)&runtime·sched.goidgen, 1);
 	if(raceenabled)
 		runtime·racegostart(goid, callerpc);

src/pkg/runtime/runtime.h

--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -203,7 +203,7 @@ struct	G
 	G*	alllink;	// on allg
 	void*	param;		// passed parameter on wakeup
 	int16	status;
-	int32	goid;
+	int64	goid;
 	uint32	selgen;		// valid sudog pointer
 	int8*	waitreason;	// if status==Gwaiting
 	G*	schedlink;

src/pkg/runtime/mgc0.c

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -567,7 +567,7 @@ addstackroots(G *gp)
 	n = 0;
 	while(stk) {
 		if(sp < guard-StackGuard || (byte*)stk < sp) {
-			runtime·printf("scanstack inconsistent: g%d#%d sp=%p not in [%p,%p]\\n", gp->goid, n, sp, guard-StackGuard, stk);
+			runtime·printf("scanstack inconsistent: g%D#%d sp=%p not in [%p,%p]\\n", gp->goid, n, sp, guard-StackGuard, stk);
 			runtime·throw("scanstack");
 		}
 		addroot(sp, (byte*)stk - sp);

コアとなるコードの解説

src/pkg/runtime/proc.cの変更

  • struct Sched内のgoidgenの型変更:

    -	int32 goidgen;
    +	int64 goidgen;
    

    goidgenは、Goランタイムのスケジューラ構造体Schedの一部であり、新しいゴルーチンに割り当てる次のIDを生成するためのカウンタです。以前は32ビット整数(int32)でしたが、この変更により64ビット整数(int64)になりました。これにより、約20億個のゴルーチンIDの制限が、約900京個という実質的に無限のID空間に拡張され、IDの枯渇やラップアラウンドの問題が解消されます。

  • runtime·newproc1関数内のアトミック操作の変更:

    -	goid = runtime·xadd((uint32*)&runtime·sched.goidgen, 1);
    +	goid = runtime·xadd64((uint64*)&runtime·sched.goidgen, 1);
    

    runtime·newproc1は、Goのgoキーワードによって新しいゴルーチンが作成される際に呼び出される内部関数です。ここで、runtime·sched.goidgenをインクリメントして新しいゴルーチンIDを取得しています。

    • runtime·xadd: 32ビット整数に対するアトミック加算関数です。
    • runtime·xadd64: 64ビット整数に対するアトミック加算関数です。 goidgenの型がint64に変更されたため、それに合わせて64ビットのアトミック加算関数であるruntime·xadd64を使用するように変更されました。これにより、複数のゴルーチンが同時にIDを要求しても、IDの生成処理が安全かつ競合なく行われることが保証されます。

src/pkg/runtime/runtime.hの変更

  • struct G内のgoidの型変更:
    -	int32	goid;
    +	int64	goid;
    
    struct Gは、個々のゴルーチンを表すGoランタイムの内部構造体です。この構造体に含まれるgoidフィールドは、そのゴルーチンの一意のIDを保持します。以前は32ビット整数(int32)でしたが、この変更により64ビット整数(int64)になりました。これにより、各ゴルーチンが64ビットのIDを保持できるようになり、システム全体で生成されるゴルーチンIDの範囲が拡張されたことに対応します。

src/pkg/runtime/mgc0.cおよびその他のトレースバック関連ファイルの変更

  • runtime·printfのフォーマット指定子変更:
    -			runtime·printf("scanstack inconsistent: g%d#%d sp=%p not in [%p,%p]\\n", gp->goid, n, sp, guard-StackGuard, stk);
    +			runtime·printf("scanstack inconsistent: g%D#%d sp=%p not in [%p,%p]\\n", gp->goid, n, sp, guard-StackGuard, stk);
    
    src/pkg/runtime/mgc0.c(ガベージコレクション関連)、src/pkg/runtime/proc.c(プロセス管理関連)、src/pkg/runtime/traceback_arm.csrc/pkg/runtime/traceback_x86.c(スタックトレースバック関連)など、ゴルーチンIDをログやデバッグ出力で表示する箇所で、runtime·printfのフォーマット指定子が%dから%Dに変更されています。 Goランタイム内部のruntime·printfは、標準Cライブラリのprintfとは異なる独自のフォーマット指定子を持つことがあります。この場合、%Dは64ビット整数(int64)を正しく出力するための指定子として導入されたと考えられます。これにより、ゴルーチンIDが64ビットに拡張された後も、デバッグ出力やエラーメッセージでIDが正しく表示されることが保証されます。

これらの変更は、Goランタイムの内部でゴルーチンIDがどのように生成、格納、そして表示されるかという、ID管理の基盤を強化するものです。

関連リンク

参考にした情報源リンク

  • Go Issue #4275 (上記に同じ)
  • Go言語のソースコード(src/pkg/runtimeディレクトリ)
  • Go言語のスケジューラに関するドキュメントや解説記事 (一般的な知識として)
  • アトミック操作に関する一般的な情報 (一般的な知識として)
  • C言語のprintfフォーマット指定子に関する情報 (一般的な知識として)
  • 32ビット/64ビット整数に関する一般的な情報 (一般的な知識として)