[インデックス 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の枯渇問題が解決され、より大規模なアプリケーションでの安定性が向上します。
具体的な変更点は以下の通りです。
-
goidgen
の型変更:src/pkg/runtime/proc.c
内のstruct Sched
において、ゴルーチンIDを生成するためのカウンタであるgoidgen
の型がint32
からint64
に変更されました。// proc.c - int32 goidgen; + int64 goidgen;
goidgen
は、新しいゴルーチンが作成されるたびにインクリメントされ、その値が新しいゴルーチンのIDとして割り当てられます。このカウンタが64ビットになることで、生成可能なゴルーチンIDの最大値が大幅に増加します。
-
goid
の型変更:src/pkg/runtime/runtime.h
内のstruct G
(ゴルーチンを表す構造体)において、各ゴルーチンが持つIDであるgoid
の型がint32
からint64
に変更されました。// runtime.h - int32 goid; + int64 goid;
- これにより、個々のゴルーチンが保持するIDも64ビットの値を格納できるようになります。
-
アトミック操作の変更:
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の生成が安全かつ正確に行われることが保証されます。
-
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.c
、src/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: https://github.com/golang/go/issues/4275
参考にした情報源リンク
- Go Issue #4275 (上記に同じ)
- Go言語のソースコード(
src/pkg/runtime
ディレクトリ) - Go言語のスケジューラに関するドキュメントや解説記事 (一般的な知識として)
- アトミック操作に関する一般的な情報 (一般的な知識として)
- C言語の
printf
フォーマット指定子に関する情報 (一般的な知識として) - 32ビット/64ビット整数に関する一般的な情報 (一般的な知識として)