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

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

このコミットは、Go言語のランタイムにおけるデータ競合検出器(Race Detector)の内部実装に関する重要な変更です。具体的には、データ競合のコンテキストを追跡するために、従来の「goroutine ID」を使用する方式から「明示的なレースコンテキスト(explicit race context)」を使用する方式へと切り替えています。これにより、同時に存在するgoroutineの最大数に関する制限が撤廃され、パフォーマンスもわずかに向上しています。

コミット

  • コミットハッシュ: 0a40cd2661a14baa9a57b4f5af84494455d83f88
  • Author: Dmitriy Vyukov dvyukov@google.com
  • Date: Wed Feb 6 11:40:54 2013 +0400

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

https://github.com/golang/go/commit/0a40cd2661a14baa9a57b4f5af84494455d83f88

元コミット内容

    runtime/race: switch to explicit race context instead of goroutine id's
    Removes limit on maximum number of goroutines ever existed.
    code.google.com/p/goexecutor tests now pass successfully.
    Also slightly improves performance.
    Before: $ time ./flate.test -test.short
    real    0m9.314s
    After:  $ time ./flate.test -test.short
    real    0m8.958s
    Fixes #4286.
    The runtime is built from llvm rev 174312.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/7218044

変更の背景

Go言語のデータ競合検出器(Race Detector)は、並行処理におけるデータ競合バグを特定するための強力なツールです。しかし、このツールには以前、同時に存在するgoroutineの数に制限がありました。具体的には、GoのRace Detectorは、同時にアクティブなgoroutineの数が8192を超えるとプログラムを終了させるという技術的な制限を抱えていました。この制限は、Race Detectorが内部的にgoroutine IDを使用してメモリアクセスを追跡していたことに起因します。多数のgoroutineが生成・終了を繰り返すようなアプリケーションでは、この制限に達し、Race Detectorが有効な状態でテストを実行できないという問題が発生していました。

このコミットは、この「goroutine IDの最大数制限」を解消することを目的としています。コミットメッセージにある Fixes #4286 は、この問題がGoのIssueトラッカーで追跡されていたことを示しています。また、code.google.com/p/goexecutor tests now pass successfully. という記述から、特定の並行処理テストスイートがこの制限によって失敗していたことが伺えます。

さらに、この変更はパフォーマンスのわずかな改善ももたらしています。コミットメッセージに示されている flate.test の実行時間の比較は、このパフォーマンス向上が実測値として確認されたことを示しています。

前提知識の解説

Go言語のGoroutine

GoroutineはGo言語における軽量な並行処理の単位です。OSのスレッドよりもはるかに軽量であり、数千、数万といった多数のgoroutineを同時に実行することが可能です。Goランタイムがgoroutineのスケジューリングを管理し、複数のgoroutineを少数のOSスレッドに多重化して実行します。

データ競合 (Data Race)

データ競合とは、複数のgoroutineが同時に同じメモリ領域にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生するバグです。データ競合はプログラムの予測不能な動作やクラッシュを引き起こす可能性があり、デバッグが非常に困難な種類のバグです。

Go Race Detector (データ競合検出器)

Go Race Detectorは、Goプログラムの実行中にデータ競合を検出するためのツールです。プログラムを特殊な方法でコンパイルし(-race フラグを使用)、実行時にメモリアクセスを監視することで、データ競合のパターンを特定します。Race Detectorは、Googleが開発したThreadSanitizer (TSan) という技術をベースにしています。

ThreadSanitizer (TSan)

ThreadSanitizerは、C/C++などの言語で書かれたプログラムのデータ競合を検出するためのランタイムツールです。GoのRace DetectorもこのTSanのランタイムライブラリを内部的に利用しています。TSanは、各スレッド(Goの場合はgoroutine)のメモリアクセス履歴を追跡し、競合の可能性を検出します。

goid (Goroutine ID)

Goランタイムは、各goroutineに一意のID(goid)を割り当てます。これは主にデバッグや内部的な追跡のために使用されます。従来のRace Detectorの実装では、このgoidをデータ競合検出のコンテキストとして利用していました。

racectx (Race Context)

このコミットで導入された「明示的なレースコンテキスト」は、racectxという形で表現されます。これは、従来のgoidに代わる、Race Detectorが内部的に使用するコンテキスト識別子です。goidが単なる一意の数値IDであるのに対し、racectxはRace Detectorがデータ競合検出に必要なより抽象的で柔軟なコンテキスト情報を持つポインタまたは数値であると推測されます。これにより、Race DetectorはgoroutineのライフサイクルやIDの再利用といった問題に縛られずに、より効率的かつスケーラブルに競合検出を行えるようになります。

技術的詳細

この変更の核心は、GoランタイムとThreadSanitizerライブラリ間のインターフェースの変更です。

  1. goidからracectxへの移行:

    • 以前は、Race Detectorの各関数(Read, Write, FuncEnter, GoStartなど)は、引数としてint32 goidを受け取っていました。これは、どのgoroutineがメモリにアクセスしたか、あるいは関数に入ったか/出たかをTSanに伝えるためでした。
    • このコミットでは、これらの関数のシグネチャが変更され、int32 goidの代わりにuintptr racectxを受け取るようになりました。uintptrはポインタを保持できる整数型であり、これによりTSanはより複雑なコンテキスト情報をポインタとして受け渡すことができるようになります。
    • g->goid(現在のgoroutineのID)が直接TSanに渡されるのではなく、g->racectxという新しいフィールドが導入され、これがTSanに渡されるようになりました。
  2. Goroutine IDの生成方法の変更:

    • 以前は、新しいgoroutineが作成される際にruntime·xadd64((uint64*)&runtime·sched.goidgen, 1)というアトミックな操作でgoidを生成していました。これは、goidgenというグローバルなカウンタをインクリメントすることで、一意のgoidを割り当てていました。この方式では、生成されたgoroutineの総数が増え続けると、goidの範囲が広がり、TSanが内部的に管理するデータ構造に負荷がかかる可能性がありました。
    • 新しい実装では、newg->goid = ++runtime·sched.goidgen; となり、goidの生成は単純なインクリメントになりました。しかし、重要なのは、このgoidが直接Race Detectorに渡されなくなったことです。代わりに、runtime·racegostart関数が新しいracectxを生成し、それをnewg->racectxに設定するようになりました。これにより、Race Detectorはgoidの数値的な制限から解放されます。
  3. Race Detectorの初期化とGoroutineの開始/終了:

    • runtime·raceinit()関数は、Race Detectorの初期化を担当します。以前はvoidを返していましたが、変更後はuintptrを返すようになり、初期化時にメインgoroutineのracectxを返すようになりました。
    • runtime·racegostart関数は、新しいgoroutineが開始される際に呼び出されます。以前は親goroutineのIDと子goroutineのIDを受け取っていましたが、変更後は親のracectxと子のracectxへのポインタを受け取るようになりました。これにより、TSanはgoroutineの親子関係をracectxベースで追跡できます。
    • runtime·racegoend関数は、goroutineが終了する際に呼び出されます。以前は終了するgoroutineのIDを受け取っていましたが、変更後は現在のgoroutineのracectxを使用するようになりました。
  4. パフォーマンスの向上:

    • goidの代わりにracectxを使用することで、TSanの内部データ構造の管理がより効率的になった可能性があります。例えば、goidが連続的でない場合や、非常に大きな値になる場合に発生する可能性のあるハッシュテーブルの衝突やメモリ割り当てのオーバーヘッドが削減されたのかもしれません。
    • また、goidの生成におけるアトミック操作の削減(runtime·xadd64の削除)も、わずかながらパフォーマンス向上に寄与している可能性があります。

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

主に以下のファイルが変更されています。

  • src/pkg/runtime/proc.c: Goランタイムのスケジューリングとgoroutine管理に関するCコード。g->racectxフィールドの導入と、Race Detector関連関数の呼び出し箇所の変更。
  • src/pkg/runtime/race.c: GoランタイムとThreadSanitizerライブラリ間のCインターフェース。Race Detector関数のシグネチャ変更(goidからracectxへ)。
  • src/pkg/runtime/race.h: Race Detector関連関数のCヘッダー。シグネチャの変更を反映。
  • src/pkg/runtime/race/race.go: Go言語からThreadSanitizerライブラリのC関数を呼び出すためのGo側のラッパー。C関数のシグネチャ変更に合わせてGo側の関数シグネチャも変更。
  • src/pkg/runtime/runtime.h: G構造体にracectxフィールドを追加。
  • src/pkg/runtime/race0.c: Race Detectorが無効な場合のダミー実装。シグネチャ変更を反映。
  • src/pkg/runtime/race/race_darwin_amd64.syso, src/pkg/runtime/race/race_linux_amd64.syso, src/pkg/runtime/race/race_windows_amd64.syso: ThreadSanitizerのコンパイル済みライブラリファイル。内部実装の変更に伴いバイナリが更新されています。

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

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -221,7 +221,7 @@ runtime·schedinit(void)
  	m->nomemprof--;
 
  	if(raceenabled)
- 		runtime·raceinit();
+ 		g->racectx = runtime·raceinit();
  }
 
  extern void main·init(void);
@@ -1340,9 +1340,8 @@ 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·xadd64((uint64*)&runtime·sched.goidgen, 1);
  	if(raceenabled)
- 		runtime·racegostart(goid, callerpc);
+ 		racectx = runtime·racegostart(callerpc);
 
  	schedlock();
 
@@ -1374,9 +1373,11 @@ runtime·newproc1(byte *fn, byte *argp, int32 narg, int32 nret, void *callerpc)
  	newg->sched.g = newg;
  	newg->entry = fn;
  	newg->gopc = (uintptr)callerpc;
+ 	if(raceenabled)
+ 		newg->racectx = racectx;
 
  	runtime·sched.gcount++;
- 	newg->goid = goid;
+ 	newg->goid = ++runtime·sched.goidgen;
 
  	newprocreadylocked(newg);
  	schedunlock();

src/pkg/runtime/race.c の変更例

--- a/src/pkg/runtime/race.c
+++ b/src/pkg/runtime/race.c
@@ -10,36 +10,36 @@
  #include "malloc.h"
  #include "race.h"
 
-void runtime∕race·Initialize(void);
+void runtime∕race·Initialize(uintptr *racectx);
 void runtime∕race·MapShadow(void *addr, uintptr size);
 void runtime∕race·Finalize(void);
-void runtime∕race·FinalizerGoroutine(int32);
-void runtime∕race·Read(int32 goid, void *addr, void *pc);
-void runtime∕race·Write(int32 goid, void *addr, void *pc);
-void runtime∕race·ReadRange(int32 goid, void *addr, uintptr sz, uintptr step, void *pc);
-void runtime∕race·WriteRange(int32 goid, void *addr, uintptr sz, uintptr step, void *pc);
-void runtime∕race·FuncEnter(int32 goid, void *pc);
-void runtime∕race·FuncExit(int32 goid);
-void runtime∕race·Malloc(int32 goid, void *p, uintptr sz, void *pc);
+void runtime∕race·FinalizerGoroutine(uintptr racectx);
+void runtime∕race·Read(uintptr racectx, void *addr, void *pc);
+void runtime∕race·Write(uintptr racectx, void *addr, void *pc);
+void runtime∕race·ReadRange(uintptr racectx, void *addr, uintptr sz, uintptr step, void *pc);
+void runtime∕race·WriteRange(uintptr racectx, void *addr, uintptr sz, uintptr step, void *pc);
+void runtime∕race·FuncEnter(uintptr racectx, void *pc);
+void runtime∕race·FuncExit(uintptr racectx);
+void runtime∕race·Malloc(uintptr racectx, void *p, uintptr sz, void *pc);
 void runtime∕race·Free(void *p);
-void runtime∕race·GoStart(int32 pgoid, int32 chgoid, void *pc);
-void runtime∕race·GoEnd(int32 goid);
-void runtime∕race·Acquire(int32 goid, void *addr);
-void runtime∕race·Release(int32 goid, void *addr);
-void runtime∕race·ReleaseMerge(int32 goid, void *addr);
+void runtime∕race·GoStart(uintptr racectx, uintptr *chracectx, void *pc);
+void runtime∕race·GoEnd(uintptr racectx);
+void runtime∕race·Acquire(uintptr racectx, void *addr);
+void runtime∕race·Release(uintptr racectx, void *addr);
+void runtime∕race·ReleaseMerge(uintptr racectx, void *addr);
 
 extern byte noptrdata[];
 extern byte enoptrbss[];
 
 static bool onstack(uintptr argp);
 
-void
+uintptr
 runtime·raceinit(void)
 {
-	uintptr sz;
+	uintptr sz, racectx;
 
 	m->racecall = true;
-	runtime∕race·Initialize();
+	runtime∕race·Initialize(&racectx);
 	sz = (byte*)&runtime·mheap - noptrdata;
 	if(sz)
 		runtime∕race·MapShadow(noptrdata, sz);
@@ -47,6 +47,7 @@ runtime·raceinit(void)
 	if(sz)
 		runtime∕race·MapShadow(&runtime·mheap+1, sz);
 	m->racecall = false;
+	return racectx;
 }
 
 void
@@ -73,7 +74,7 @@ runtime·racewrite(uintptr addr)
 {
 	if(!onstack(addr)) {
 		m->racecall = true;
-		runtime∕race·Write(g->goid-1, (void*)addr, runtime·getcallerpc(&addr));
+		runtime∕race·Write(g->racectx, (void*)addr, runtime·getcallerpc(&addr));
 		m->racecall = false;
 	}
 }
@@ -86,7 +87,7 @@ runtime·raceread(uintptr addr)
 {
 	if(!onstack(addr)) {
 		m->racecall = true;
-		runtime∕race·Read(g->goid-1, (void*)addr, runtime·getcallerpc(&addr));
+		runtime∕race·Read(g->racectx, (void*)addr, runtime·getcallerpc(&addr));
 		m->racecall = false;
 	}
 }
@@ -105,7 +106,7 @@ runtime·racefuncenter(uintptr pc)
 		runtime·callers(2, &pc, 1);
 
 	m->racecall = true;
-	runtime∕race·FuncEnter(g->goid-1, (void*)pc);
+	runtime∕race·FuncEnter(g->racectx, (void*)pc);
 	m->racecall = false;
 }
 
@@ -115,7 +116,7 @@ void
 runtime·racefuncexit(void)
 {
 	m->racecall = true;
-	runtime∕race·FuncExit(g->goid-1);
+	runtime∕race·FuncExit(g->racectx);
 	m->racecall = false;
 }
 
@@ -126,7 +127,7 @@ runtime·racemalloc(void *p, uintptr sz, void *pc)
 	if(m->curg == nil)
 		return;
 	m->racecall = true;
-	runtime∕race·Malloc(m->curg->goid-1, p, sz, pc);
+	runtime∕race·Malloc(m->curg->racectx, p, sz, pc);
 	m->racecall = false;
 }
 
@@ -138,42 +139,45 @@ runtime·racefree(void *p)
 	m->racecall = false;
 }
 
-void
-runtime·racegostart(int32 goid, void *pc)
+uintptr
+runtime·racegostart(void *pc)
 {
+	uintptr racectx;
+
 	m->racecall = true;
-	runtime∕race·GoStart(g->goid-1, goid-1, pc);
+	runtime∕race·GoStart(g->racectx, &racectx, pc);
 	m->racecall = false;
+	return racectx;
 }
 
 void
-runtime·racegoend(int32 goid)
+runtime·racegoend(void)
 {
 	m->racecall = true;
-	runtime∕race·GoEnd(goid-1);
+	runtime∕race·GoEnd(g->racectx);
 	m->racecall = false;
 }
 
 static void
 memoryaccess(void *addr, uintptr callpc, uintptr pc, bool write)
 {
-	int64 goid;
+	uintptr racectx;
 
 	if(!onstack((uintptr)addr)) {
 		m->racecall = true;
-		goid = g->goid-1;
+		racectx = g->racectx;
 		if(callpc) {
 			if(callpc == (uintptr)runtime·lessstack ||
 				(callpc >= (uintptr)runtime·mheap.arena_start && callpc < (uintptr)runtime·mheap.arena_used))
 				runtime·callers(3, &callpc, 1);
-			runtime∕race·FuncEnter(goid, (void*)callpc);
+			runtime∕race·FuncEnter(racectx, (void*)callpc);
 		}
 		if(write)
-			runtime∕race·Write(goid, addr, (void*)pc);
+			runtime∕race·Write(racectx, addr, (void*)pc);
 		else
-			runtime∕race·Read(goid, addr, (void*)pc);
+			runtime∕race·Read(racectx, addr, (void*)pc);
 		if(callpc)
-			runtime∕race·FuncExit(goid);
+			runtime∕race·FuncExit(racectx);
 		m->racecall = false;
 	}
 }
@@ -183,23 +187,23 @@ runtime·racereadpc(void *addr, void *callpc, void *pc)
 static void
 rangeaccess(void *addr, uintptr size, uintptr step, uintptr callpc, uintptr pc, bool write)
 {
-	int64 goid;
+	uintptr racectx;
 
 	if(!onstack((uintptr)addr)) {
 		m->racecall = true;
-		goid = g->goid-1;
+		racectx = g->racectx;
 		if(callpc) {
 			if(callpc == (uintptr)runtime·lessstack ||
 				(callpc >= (uintptr)runtime·mheap.arena_start && callpc < (uintptr)runtime·mheap.arena_used))
 				runtime·callers(3, &callpc, 1);
-			runtime∕race·FuncEnter(goid, (void*)callpc);
+			runtime∕race·FuncEnter(racectx, (void*)callpc);
 		}
 		if(write)
-			runtime∕race·WriteRange(goid, addr, size, step, (void*)pc);
+			runtime∕race·WriteRange(racectx, addr, size, step, (void*)pc);
 		else
-			runtime∕race·ReadRange(goid, addr, size, step, (void*)pc);
+			runtime∕race·ReadRange(racectx, addr, size, step, (void*)pc);
 		if(callpc)
-			runtime∕race·FuncExit(goid);
+			runtime∕race·FuncExit(racectx);
 		m->racecall = false;
 	}
 }
@@ -218,7 +222,7 @@ runtime·raceacquireg(G *gp, void *addr)
 	if(g->raceignore)
 		return;
 	m->racecall = true;
-	runtime∕race·Acquire(gp->goid-1, addr);
+	runtime∕race·Acquire(gp->racectx, addr);
 	m->racecall = false;
 }
 
@@ -234,7 +238,7 @@ runtime·racereleaseg(G *gp, void *addr)
 	if(g->raceignore)
 		return;
 	m->racecall = true;
-	runtime∕race·Release(gp->goid-1, addr);
+	runtime∕race·Release(gp->racectx, addr);
 	m->racecall = false;
 }
 
@@ -250,7 +254,7 @@ runtime·racereleasemergeg(G *gp, void *addr)
 	if(g->raceignore)
 		return;
 	m->racecall = true;
-	runtime∕race·ReleaseMerge(gp->goid-1, addr);
+	runtime∕race·ReleaseMerge(gp->racectx, addr);
 	m->racecall = false;
 }
 
@@ -258,7 +262,7 @@ void
 runtime·racefingo(void)
 {
 	m->racecall = true;
-	runtime∕race·FinalizerGoroutine(g->goid - 1);
+	runtime∕race·FinalizerGoroutine(g->racectx);
 	m->racecall = false;
 }

コアとなるコードの解説

src/pkg/runtime/proc.c

  • runtime·schedinit(): ランタイム初期化時に、メインgoroutineのracectxg->racectxに設定するように変更されました。runtime·raceinit()uintptrを返すようになったため、その戻り値をg->racectxに代入しています。
  • runtime·goexit(): goroutineが終了する際にruntime·racegoend()を呼び出すようになりました。以前はschedule()関数内でgoidを渡していましたが、racectxベースになったため、g->racectxを暗黙的に使用するruntime·racegoend()が直接呼び出される形になりました。
  • schedule(): Gmoribund(終了状態のgoroutine)の処理から、runtime·racegoend(gp->goid)の呼び出しが削除されました。これは、runtime·goexit()で処理されるようになったためです。
  • runtime·newproc1():
    • int64 goid;uintptr racectx; に変更されました。
    • goid = runtime·xadd64((uint64*)&runtime·sched.goidgen, 1); というアトミックなgoid生成ロジックが削除されました。
    • runtime·racegostart(goid, callerpc); の呼び出しが racectx = runtime·racegostart(callerpc); に変更され、新しいracectxが返されるようになりました。
    • newg->racectx = racectx; という行が追加され、新しく作成されるgoroutineのG構造体にracectxが設定されるようになりました。
    • newg->goid = goid;newg->goid = ++runtime·sched.goidgen; に変更されました。これは、goid自体は引き続き生成されますが、Race Detectorのコンテキストとしては使用されなくなったことを意味します。goidは主にデバッグ目的で残されていると考えられます。

src/pkg/runtime/race.c

  • runtime∕race·Initializeruntime∕race·FinalizerGoroutineruntime∕race·Readruntime∕race·Writeruntime∕race·ReadRangeruntime∕race·WriteRangeruntime∕race·FuncEnterruntime∕race·FuncExitruntime∕race·Mallocruntime∕race·GoStartruntime∕race·GoEndruntime∕race·Acquireruntime∕race·Releaseruntime∕race·ReleaseMergeといった、ThreadSanitizerのC関数を呼び出すGoランタイム側のラッパー関数のシグネチャが、int32 goidからuintptr racectxへと一斉に変更されています。
  • runtime·raceinit(): 戻り値がvoidからuintptrに変更され、初期化されたracectxを返すようになりました。
  • runtime·racegostart(): 戻り値がvoidからuintptrに変更され、新しく開始されるgoroutineのracectxを返すようになりました。また、引数も親のracectxと子のracectxへのポインタを受け取るように変更されました。
  • runtime·racegoend(): 引数からint32 goidが削除され、現在のgoroutineのracectxを暗黙的に使用するようになりました。
  • memoryaccess() および rangeaccess(): これらのヘルパー関数も、内部でgoidの代わりにracectxを使用するように変更されました。

src/pkg/runtime/runtime.h

  • struct G (goroutine構造体) に uintptr racectx; フィールドが追加されました。これにより、各goroutineが自身のRace Detectorコンテキストを保持できるようになります。

src/pkg/runtime/race/race.go

  • CGOのimport "C"ブロック内のC関数宣言が、int goidからvoid *racectxへと変更されています。これは、Go側からCのThreadSanitizer関数を呼び出す際の型定義を更新したものです。void *はGoのunsafe.Pointerに対応し、uintptrをポインタとして扱うことを可能にします。
  • Go側のラッパー関数(Initialize, FinalizerGoroutine, Read, Writeなど)のシグネチャも、C側の変更に合わせてgoid int32からracectx uintptrへと変更されています。

これらの変更により、Race DetectorはgoroutineのIDに依存することなく、より抽象的な「レースコンテキスト」に基づいてデータ競合を検出できるようになりました。これにより、goroutineの最大数制限が解消され、大規模な並行アプリケーションでもRace Detectorをより効果的に利用できるようになります。また、コンテキスト管理の効率化により、わずかながらパフォーマンスも向上しています。

関連リンク

参考にした情報源リンク