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

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

このコミットは、Goランタイムのデータ競合検出器(Race Detector)がselectステートメント内のチャネル操作におけるリード/ライト競合を正しく検出できるようにするための重要な修正を導入しています。具体的には、selectケース内で発生するメモリアクセスを適切に計測し、既存のテストが検出できなかった競合状態を捕捉することを目的としています。

コミット

commit cb86d867866514bb751e1caa16425002db54e303
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Jan 22 10:36:17 2014 +0400

    runtime/race: race instrument reads/writes in select cases
    The new select tests currently fail (the race is not detected).
    
    R=khr
    CC=golang-codereviews
    https://golang.org/cl/54220043

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

https://github.com/golang/go/commit/cb86d867866514bb751e1caa16425002db54e303

元コミット内容

runtime/race: race instrument reads/writes in select cases
The new select tests currently fail (the race is not detected).

R=khr
CC=golang-codereviews
https://golang.org/cl/54220043

変更の背景

Goの並行処理モデルにおいて、チャネルはゴルーチン間の安全な通信と同期の主要な手段です。しかし、チャネルを介したデータの送受信が、チャネル外の共有メモリへのアクセスと組み合わされる場合、データ競合が発生する可能性があります。Goのデータ競合検出器は、このような競合を特定し、開発者が潜在的なバグを修正できるように設計されています。

このコミットが導入される以前は、selectステートメント内で実行されるチャネル操作(送信または受信)に関連するメモリアクセスが、データ競合検出器によって適切に計測されていませんでした。コミットメッセージにある「The new select tests currently fail (the race is not detected).」という記述は、新しく追加されたselect関連のテストケースが、実際にはデータ競合を含んでいるにもかかわらず、既存の競合検出器がそれを報告できなかったことを示しています。これは、selectステートメントの複雑な実行フローと、その内部でのメモリ操作のタイミングが、従来の計測ロジックでは十分に考慮されていなかったためと考えられます。

この問題は、selectステートメントが複数のチャネル操作を同時に待機し、そのうちの1つが準備できたときに実行されるという性質上、特に重要です。どのケースが選択されるかによって、異なるメモリアクセスパターンが発生する可能性があり、これらのアクセスが共有変数に対して適切に同期されていない場合、競合状態に陥ります。このコミットは、selectケース内のチャネル操作が引き起こすメモリリード/ライトを正確に計測することで、競合検出器の精度と信頼性を向上させることを目的としています。

前提知識の解説

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

Goのデータ競合検出器は、GoogleのThreadSanitizer (TSan) をベースにしており、並行プログラムにおけるデータ競合を特定するための強力なツールです。データ競合とは、少なくとも1つの書き込み操作を含む2つ以上のメモリ操作が、異なるゴルーチンによって同時に実行され、かつそれらの操作間に明確な「happens-before」関係がない場合に発生します。データ競合は、予測不能なプログラムの動作やバグの主要な原因となります。

競合検出器は、プログラムの実行時に以下のメカニズムで動作します。

  1. メモリアクセスの計測: -raceフラグを付けてGoプログラムをコンパイルすると、コンパイラはすべてのメモリリード/ライト操作にフックを追加します。これらのフックは、アクセス時刻、ゴルーチンID、メモリアドレス、スタックトレースなどのメタデータを記録します。
  2. 同期イベントの追跡: チャネル操作、ミューテックス(sync.Mutex)、アトミック操作(sync/atomic)などのGoの並行処理プリミティブは、競合検出器によって「happens-before」関係を確立するメカニズムとして認識されます。例えば、チャネルへの送信操作は、対応する受信操作が完了する「前に発生する」とGoメモリモデルで定義されています。
  3. 競合の検出: 競合検出器は、記録されたメモリアクセスと同期イベントのメタデータを分析し、happens-before関係によって順序付けられていない競合するメモリ操作のペアを特定します。競合が検出されると、競合検出器は詳細なレポート(スタックトレースを含む)を出力し、問題の特定を支援します。

Goのチャネルとselectステートメント

  • チャネル: Goにおけるチャネルは、ゴルーチン間で値を送受信するための通信パイプです。チャネルは、値の送信と受信の両方で同期メカニズムを提供し、データ競合を避けるのに役立ちます。
    • バッファなしチャネル: 送信側は受信側が準備できるまでブロックし、受信側は送信側が準備できるまでブロックします。これにより、送信と受信が同時に行われることが保証されます。
    • バッファありチャネル: バッファが満杯になるまで送信側はブロックせず、バッファが空になるまで受信側はブロックしません。
  • selectステートメント: selectステートメントは、複数のチャネル操作を同時に待機し、そのうちの1つが準備できたときに実行することを可能にします。
    • selectは、準備ができたcaseが複数ある場合、ランダムに1つを選択します。
    • どのcaseも準備ができていない場合、defaultケースがあればそれが実行されます。defaultケースがなければ、selectはチャネル操作のいずれかが準備できるまでブロックします。

selectステートメントの内部では、チャネル操作が実行される際に、そのチャネルを介して値がコピーされたり、共有メモリがアクセスされたりします。これらの操作が、selectステートメントの外部で発生する他のメモリアクセスと適切に同期されていない場合、データ競合が発生する可能性があります。

技術的詳細

このコミットの主要な技術的変更点は、Goランタイムのチャネル実装(src/pkg/runtime/chan.c)において、selectステートメント内のチャネル操作に関連するメモリリード/ライトを、データ競合検出器が認識できるようにするための計測コードを追加したことです。

  1. Hchan構造体の変更:

    • Hchan構造体(チャネルの内部表現)のelemalgフィールドがType* elemtypeに変更されました。
    • elemalgは要素の型に特化したアルゴリズム(コピー、プリントなど)を指すポインタでしたが、elemtypeは要素の型情報全体を指すポインタになりました。これにより、競合検出器がオブジェクトの型情報にアクセスしやすくなり、より正確な計測が可能になります。
    • これに伴い、elemalg->copyelemalg->printといった呼び出しは、elemtype->alg->copyelemtype->alg->printに変更されています。
  2. selectケースにおける競合検出器の計測追加:

    • runtime·selectgo関数(selectステートメントのランタイム実装)内で、チャネル操作が実行される直前に、runtime·racewriteobjectpcおよびruntime·racereadobjectpc関数が呼び出されるようになりました。
    • これらの関数は、特定のメモリ領域への書き込み(CaseRecv、受信ケースで受信バッファに書き込む場合)または読み込み(CaseSend、送信ケースで送信バッファから読み込む場合)を競合検出器に通知します。
    • 特に、selectステートメントの非同期受信(asyncrecv)、非同期送信(asyncsend)、同期受信(syncrecv)、同期送信(syncsend)の各パスにおいて、これらの計測が追加されています。
    • runtime·racewriteobjectpc(cas->sg.elem, c->elemtype, cas->pc, runtime·chanrecv): 受信ケースで、受信した値をcas->sg.elemに書き込む際に、そのメモリ操作を競合検出器に通知します。c->elemtypeは要素の型情報、cas->pcはプログラムカウンタ(呼び出し元のコード位置)、runtime·chanrecvは関連するランタイム関数を示します。
    • runtime·racereadobjectpc(cas->sg.elem, c->elemtype, cas->pc, runtime·chansend): 送信ケースで、送信する値をcas->sg.elemから読み出す際に、そのメモリ操作を競合検出器に通知します。
  3. 新しいテストケースの追加:

    • src/pkg/runtime/race/testdata/chan_test.goに、selectステートメントを含むチャネル操作におけるデータ競合を意図的に発生させる新しいテストケースが多数追加されました。
    • TestRaceSelectReadWriteAsync, TestRaceSelectReadWriteSync: selectステートメント内で共有変数xへのリード/ライト競合を発生させるテスト。
    • TestNoRaceSelectReadWriteAsync: 競合が発生しないことを確認するテスト。
    • TestRaceChanReadWriteAsync, TestRaceChanReadWriteSync: selectを使用しない通常のチャネル操作におけるリード/ライト競合テスト。
    • TestNoRaceChanReadWriteAsync: 競合が発生しないことを確認するテスト。 これらのテストは、修正が正しく機能し、以前検出されなかった競合を報告できることを検証するために不可欠です。

これらの変更により、競合検出器はselectステートメントの内部で発生するメモリ操作をより詳細に監視できるようになり、Goの並行プログラムにおけるデータ競合の検出能力が向上しました。

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

src/pkg/runtime/chan.c

  • Hchan構造体のelemalgフィールドをelemtypeに変更し、関連する参照を更新。
  • runtime·selectgo関数内のasyncrecv, asyncsend, syncrecv, syncsendの各セクションに、raceenabledが真の場合にruntime·racewriteobjectpcおよびruntime·racereadobjectpcの呼び出しを追加。
--- a/src/pkg/runtime/chan.c
+++ b/src/pkg/runtime/chan.c
@@ -41,7 +41,7 @@ struct	Hchan
 	uint16	elemsize;
 	uint16	pad;			// ensures proper alignment of the buffer that follows Hchan in memory
 	bool	closed;
-	Alg*	elemalg;		// interface for element type
+	Type*	elemtype;		// element type
 	uintgo	sendx;			// send index
 	uintgo	recvx;			// receive index
 	WaitQ	recvq;			// list of recv waiters
@@ -110,7 +110,7 @@ runtime·makechan_c(ChanType *t, int64 hint)
 	// allocate memory in one call
 	c = (Hchan*)runtime·mallocgc(sizeof(*c) + hint*elem->size, (uintptr)t | TypeInfo_Chan, 0);
 	c->elemsize = elem->size;
-	c->elemalg = elem->alg;
+	c->elemtype = elem;
 	c->dataqsiz = hint;
 
 	if(debug)
@@ -174,7 +174,7 @@ runtime·chansend(ChanType *t, Hchan *c, byte *ep, bool *pres, void *pc)
 
 	if(debug) {
 		runtime·printf("chansend: chan=%p; elem=", c);
-		c->elemalg->print(c->elemsize, ep);
+		c->elemtype->alg->print(c->elemsize, ep);
 		runtime·prints("\n");
 	}
 
@@ -203,7 +203,7 @@ runtime·chansend(ChanType *t, Hchan *c, byte *ep, bool *pres, void *pc)
 		gp = sg->g;
 		gp->param = sg;
 		if(sg->elem != nil)
-			c->elemalg->copy(c->elemsize, sg->elem, ep);
+			c->elemtype->alg->copy(c->elemsize, sg->elem, ep);
 		if(sg->releasetime)
 			sg->releasetime = runtime·cputicks();
 		runtime·ready(gp);
@@ -261,7 +261,7 @@ asynch:
 	if(raceenabled)
 		runtime·racerelease(chanbuf(c, c->sendx));
 
-	c->elemalg->copy(c->elemsize, chanbuf(c, c->sendx), ep);
+	c->elemtype->alg->copy(c->elemsize, chanbuf(c, c->sendx), ep);
 	if(++c->sendx == c->dataqsiz)
 		c->sendx = 0;
 	c->qcount++;
@@ -331,7 +331,7 @@ runtime·chanrecv(ChanType *t, Hchan* c, byte *ep, bool *selected, bool *receive
 		runtime·unlock(c);
 
 		if(ep != nil)
-			c->elemalg->copy(c->elemsize, ep, sg->elem);
+			c->elemtype->alg->copy(c->elemsize, ep, sg->elem);
 		gp = sg->g;
 		gp->param = sg;
 		if(sg->releasetime)
@@ -397,8 +397,8 @@ asynch:
 		runtime·raceacquire(chanbuf(c, c->recvx));
 
 	if(ep != nil)
-		c->elemalg->copy(c->elemsize, ep, chanbuf(c, c->recvx));
-	c->elemalg->copy(c->elemsize, chanbuf(c, c->recvx), nil);
+		c->elemtype->alg->copy(c->elemsize, ep, chanbuf(c, c->recvx));
+	c->elemtype->alg->copy(c->elemsize, chanbuf(c, c->recvx), nil);
 	if(++c->recvx == c->dataqsiz)
 		c->recvx = 0;
 	c->qcount--;
@@ -423,7 +423,7 @@ asynch:
 
 closed:
 	if(ep != nil)
-		c->elemalg->copy(c->elemsize, ep, nil);
+		c->elemtype->alg->copy(c->elemsize, ep, nil);
 	if(selected != nil)
 		*selected = true;
 	if(received != nil)
@@ -1007,18 +1007,28 @@ loop:
 			*cas->receivedp = true;
 	}
 
+	if(raceenabled) {
+		if(cas->kind == CaseRecv && cas->sg.elem != nil)
+			runtime·racewriteobjectpc(cas->sg.elem, c->elemtype, cas->pc, runtime·chanrecv);
+		else if(cas->kind == CaseSend)
+			runtime·racereadobjectpc(cas->sg.elem, c->elemtype, cas->pc, runtime·chansend);
+	}
+
 	selunlock(sel);
 	goto retc;
 
 asyncrecv:
 	// can receive from buffer
-	if(raceenabled)
+	if(raceenabled) {
+		if(cas->sg.elem != nil)
+			runtime·racewriteobjectpc(cas->sg.elem, c->elemtype, cas->pc, runtime·chanrecv);
 		runtime·raceacquire(chanbuf(c, c->recvx));
+	}
 	if(cas->receivedp != nil)
 		*cas->receivedp = true;
 	if(cas->sg.elem != nil)
-		c->elemalg->copy(c->elemsize, cas->sg.elem, chanbuf(c, c->recvx));
-	c->elemalg->copy(c->elemsize, chanbuf(c, c->recvx), nil);
+		c->elemtype->alg->copy(c->elemsize, cas->sg.elem, chanbuf(c, c->recvx));
+	c->elemtype->alg->copy(c->elemsize, chanbuf(c, c->recvx), nil);
 	if(++c->recvx == c->dataqsiz)
 		c->recvx = 0;
 	c->qcount--;
@@ -1036,9 +1046,11 @@ asyncrecv:
 
 asyncsend:
 	// can send to buffer
-	if(raceenabled)
+	if(raceenabled) {
 		runtime·racerelease(chanbuf(c, c->sendx));
-	c->elemalg->copy(c->elemsize, chanbuf(c, c->sendx), cas->sg.elem);
+		runtime·racereadobjectpc(cas->sg.elem, c->elemtype, cas->pc, runtime·chansend);
+	}
+	c->elemtype->alg->copy(c->elemsize, chanbuf(c, c->sendx), cas->sg.elem);
 	if(++c->sendx == c->dataqsiz)
 		c->sendx = 0;
 	c->qcount++;
@@ -1056,15 +1068,18 @@ asyncsend:
 
 syncrecv:
 	// can receive from sleeping sender (sg)
-	if(raceenabled)
+	if(raceenabled) {
+		if(cas->sg.elem != nil)
+			runtime·racewriteobjectpc(cas->sg.elem, c->elemtype, cas->pc, runtime·chanrecv);
 		tracesync(c, sg);
+	}
 	selunlock(sel);
 	if(debug)
 		runtime·printf("syncrecv: sel=%p c=%p o=%d\n", sel, c, o);
 	if(cas->receivedp != nil)
 		*cas->receivedp = true;
 	if(cas->sg.elem != nil)
-		c->elemalg->copy(c->elemsize, cas->sg.elem, sg->elem);
+		c->elemtype->alg->copy(c->elemsize, cas->sg.elem, sg->elem);
 	gp = sg->g;
 	gp->param = sg;
 	if(sg->releasetime)
@@ -1078,20 +1093,22 @@ rclose:
 	if(cas->receivedp != nil)
 		*cas->receivedp = false;
 	if(cas->sg.elem != nil)
-		c->elemalg->copy(c->elemsize, cas->sg.elem, nil);
+		c->elemtype->alg->copy(c->elemsize, cas->sg.elem, nil);
 	if(raceenabled)
 		runtime·raceacquire(c);
 	goto retc;
 
 syncsend:
 	// can send to sleeping receiver (sg)
-	if(raceenabled)
+	if(raceenabled) {
+		runtime·racereadobjectpc(cas->sg.elem, c->elemtype, cas->pc, runtime·chansend);
 		tracesync(c, sg);
+	}
 	selunlock(sel);
 	if(debug)
 		runtime·printf("syncsend: sel=%p c=%p o=%d\n", sel, c, o);
 	if(sg->elem != nil)
-		c->elemalg->copy(c->elemsize, sg->elem, cas->sg.elem);
+		c->elemtype->alg->copy(c->elemsize, sg->elem, cas->sg.elem);
 	gp = sg->g;
 	gp->param = sg;
 	if(sg->releasetime)

src/pkg/runtime/race/testdata/chan_test.go

  • TestRaceSelectReadWriteAsync
  • TestRaceSelectReadWriteSync
  • TestNoRaceSelectReadWriteAsync
  • TestRaceChanReadWriteAsync
  • TestRaceChanReadWriteSync
  • TestNoRaceChanReadWriteAsync

これらのテスト関数が追加されています。各テストは、selectステートメントまたは通常のチャネル操作を介して共有変数xへのリード/ライト競合を意図的に発生させるか、または競合が発生しないことを確認するように設計されています。

コアとなるコードの解説

このコミットの核心は、src/pkg/runtime/chan.c内のruntime·selectgo関数における変更です。この関数はGoのselectステートメントのランタイム実装であり、複数のチャネル操作の中から準備ができたものを選択し、実行する役割を担っています。

変更のポイントは以下の通りです。

  1. Hchan構造体のelemalgからelemtypeへの変更:

    • 以前はHchan構造体にはAlg* elemalgというフィールドがあり、これはチャネル要素の型に特化したアルゴリズム(例えば、要素のコピーやプリントの方法)へのポインタでした。
    • このコミットでは、これがType* elemtypeに変更されました。TypeはGoの型システムにおける型記述子であり、単なるアルゴリズムの集合ではなく、型のサイズ、アライメント、そしてその型に関連するアルゴリズム(algフィールド)など、より包括的な情報を含んでいます。
    • この変更により、競合検出器はチャネル要素の型に関するより豊富なコンテキストにアクセスできるようになります。runtime·racewriteobjectpcruntime·racereadobjectpcのような関数は、競合検出のためにオブジェクトの正確な型情報(サイズや構造など)を必要とすることがあります。elemtypeへの変更は、この情報へのアクセスを容易にし、競合検出の精度を高めるのに役立ちます。
  2. runtime·racewriteobjectpcruntime·racereadobjectpcの導入:

    • これらの関数は、Goのデータ競合検出器の内部APIです。
    • runtime·racewriteobjectpc(addr, type, pc, callpc): 指定されたaddr(アドレス)にあるオブジェクトへの書き込み操作を競合検出器に通知します。typeはそのオブジェクトの型情報、pcは書き込みが発生したプログラムカウンタ、callpcは関連する呼び出し元のプログラムカウンタです。
    • runtime·racereadobjectpc(addr, type, pc, callpc): 指定されたaddrにあるオブジェクトからの読み込み操作を競合検出器に通知します。引数はracewriteobjectpcと同様です。
    • これらの関数は、selectステートメントの各caseが実行される際に、チャネルを介してデータが送受信されるメモリ領域に対して呼び出されます。

    具体的には、以下のシナリオでこれらの関数が呼び出されます。

    • CaseRecv (受信ケース):

      • if(cas->kind == CaseRecv && cas->sg.elem != nil): 受信ケースが選択され、かつ受信バッファ(cas->sg.elem)がnilでない場合(つまり、実際に値が受信される場合)、runtime·racewriteobjectpcが呼び出されます。これは、受信した値がcas->sg.elemに書き込まれるため、その書き込み操作を計測するためです。
      • asyncrecv (非同期受信): バッファ付きチャネルからの非同期受信時にも、同様にruntime·racewriteobjectpcが呼び出されます。
      • syncrecv (同期受信): 送信側が準備できている同期受信時にも、runtime·racewriteobjectpcが呼び出されます。
    • CaseSend (送信ケース):

      • else if(cas->kind == CaseSend): 送信ケースが選択された場合、runtime·racereadobjectpcが呼び出されます。これは、送信される値がcas->sg.elemから読み出されるため、その読み込み操作を計測するためです。
      • asyncsend (非同期送信): バッファ付きチャネルへの非同期送信時にも、同様にruntime·racereadobjectpcが呼び出されます。
      • syncsend (同期送信): 受信側が準備できている同期送信時にも、runtime·racereadobjectpcが呼び出されます。

これらの変更により、selectステートメントの内部でチャネルを介してデータが移動する際に発生するメモリリード/ライトが、競合検出器によって正確に追跡されるようになります。これにより、selectステートメントが関与する複雑な並行処理パターンにおいても、データ競合を確実に検出できるようになり、Goプログラムの信頼性とデバッグ可能性が向上します。

関連リンク

参考にした情報源リンク

  • Go race detector, built upon Google's ThreadSanitizer (TSan), effectively identifies data races within select statements by leveraging its deep understanding of Go's memory model and the synchronization properties of channel operations. (hashnode.dev)
  • The Go Memory Model explicitly defines that a send operation on a channel "happens-before" the corresponding receive operation from that channel completes. (stackademic.com)
  • select Statement Handling: A select statement in Go allows a goroutine to wait on multiple communication operations (channel sends or receives). Each case within a select block represents a potential channel operation. When a case is chosen and its channel operation executes, the race detector treats this as a synchronization event. (go.dev)
  • Race Detection within select Contexts: If a select statement's chosen case leads to an unsynchronized access to shared memory—meaning a memory access that is not properly ordered by the channel communication or any other synchronization primitive—the race detector will flag it. (go.dev)
  • Instrumentation of Memory Accesses: When a Go program is compiled with the -race flag, the compiler instruments all memory reads and writes. This instrumentation adds hooks that record metadata for each memory access, including the time of access, the goroutine ID, the memory address, and the stack trace. (llvm.org)
  • Tracking Synchronization Events: The race detector doesn't just track memory accesses; it also tracks synchronization events. Go's concurrency primitives, such as channels, mutexes (sync.Mutex), and atomic operations (sync/atomic), are recognized by the race detector as mechanisms that establish "happens-before" relationships. (dev.to)
  • In essence, the Go race detector doesn't require a unique mechanism for select statements. Instead, it relies on its comprehensive instrumentation of individual channel operations (sends and receives) within the select's cases and how these operations contribute to the overall happens-before ordering of events in the program. (go.dev)
  • If a data race occurs due to a lack of proper synchronization around or within a select statement's executed branch, the race detector will identify it because the conflicting memory accesses will lack the necessary happens-before ordering. (go.dev)