[インデックス 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」関係がない場合に発生します。データ競合は、予測不能なプログラムの動作やバグの主要な原因となります。
競合検出器は、プログラムの実行時に以下のメカニズムで動作します。
- メモリアクセスの計測:
-race
フラグを付けてGoプログラムをコンパイルすると、コンパイラはすべてのメモリリード/ライト操作にフックを追加します。これらのフックは、アクセス時刻、ゴルーチンID、メモリアドレス、スタックトレースなどのメタデータを記録します。 - 同期イベントの追跡: チャネル操作、ミューテックス(
sync.Mutex
)、アトミック操作(sync/atomic
)などのGoの並行処理プリミティブは、競合検出器によって「happens-before」関係を確立するメカニズムとして認識されます。例えば、チャネルへの送信操作は、対応する受信操作が完了する「前に発生する」とGoメモリモデルで定義されています。 - 競合の検出: 競合検出器は、記録されたメモリアクセスと同期イベントのメタデータを分析し、happens-before関係によって順序付けられていない競合するメモリ操作のペアを特定します。競合が検出されると、競合検出器は詳細なレポート(スタックトレースを含む)を出力し、問題の特定を支援します。
Goのチャネルとselect
ステートメント
- チャネル: Goにおけるチャネルは、ゴルーチン間で値を送受信するための通信パイプです。チャネルは、値の送信と受信の両方で同期メカニズムを提供し、データ競合を避けるのに役立ちます。
- バッファなしチャネル: 送信側は受信側が準備できるまでブロックし、受信側は送信側が準備できるまでブロックします。これにより、送信と受信が同時に行われることが保証されます。
- バッファありチャネル: バッファが満杯になるまで送信側はブロックせず、バッファが空になるまで受信側はブロックしません。
select
ステートメント:select
ステートメントは、複数のチャネル操作を同時に待機し、そのうちの1つが準備できたときに実行することを可能にします。select
は、準備ができたcase
が複数ある場合、ランダムに1つを選択します。- どの
case
も準備ができていない場合、default
ケースがあればそれが実行されます。default
ケースがなければ、select
はチャネル操作のいずれかが準備できるまでブロックします。
select
ステートメントの内部では、チャネル操作が実行される際に、そのチャネルを介して値がコピーされたり、共有メモリがアクセスされたりします。これらの操作が、select
ステートメントの外部で発生する他のメモリアクセスと適切に同期されていない場合、データ競合が発生する可能性があります。
技術的詳細
このコミットの主要な技術的変更点は、Goランタイムのチャネル実装(src/pkg/runtime/chan.c
)において、select
ステートメント内のチャネル操作に関連するメモリリード/ライトを、データ競合検出器が認識できるようにするための計測コードを追加したことです。
-
Hchan
構造体の変更:Hchan
構造体(チャネルの内部表現)のelemalg
フィールドがType* elemtype
に変更されました。elemalg
は要素の型に特化したアルゴリズム(コピー、プリントなど)を指すポインタでしたが、elemtype
は要素の型情報全体を指すポインタになりました。これにより、競合検出器がオブジェクトの型情報にアクセスしやすくなり、より正確な計測が可能になります。- これに伴い、
elemalg->copy
やelemalg->print
といった呼び出しは、elemtype->alg->copy
やelemtype->alg->print
に変更されています。
-
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
から読み出す際に、そのメモリ操作を競合検出器に通知します。
-
新しいテストケースの追加:
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
ステートメントのランタイム実装であり、複数のチャネル操作の中から準備ができたものを選択し、実行する役割を担っています。
変更のポイントは以下の通りです。
-
Hchan
構造体のelemalg
からelemtype
への変更:- 以前は
Hchan
構造体にはAlg* elemalg
というフィールドがあり、これはチャネル要素の型に特化したアルゴリズム(例えば、要素のコピーやプリントの方法)へのポインタでした。 - このコミットでは、これが
Type* elemtype
に変更されました。Type
はGoの型システムにおける型記述子であり、単なるアルゴリズムの集合ではなく、型のサイズ、アライメント、そしてその型に関連するアルゴリズム(alg
フィールド)など、より包括的な情報を含んでいます。 - この変更により、競合検出器はチャネル要素の型に関するより豊富なコンテキストにアクセスできるようになります。
runtime·racewriteobjectpc
やruntime·racereadobjectpc
のような関数は、競合検出のためにオブジェクトの正確な型情報(サイズや構造など)を必要とすることがあります。elemtype
への変更は、この情報へのアクセスを容易にし、競合検出の精度を高めるのに役立ちます。
- 以前は
-
runtime·racewriteobjectpc
とruntime·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プログラムの信頼性とデバッグ可能性が向上します。
関連リンク
- https://github.com/golang/go/commit/cb86d867866514bb751e1caa16425002db54e303
- https://golang.org/cl/54220043 (Go Code Review)
参考にした情報源リンク
- 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: Aselect
statement in Go allows a goroutine to wait on multiple communication operations (channel sends or receives). Eachcase
within aselect
block represents a potential channel operation. When acase
is chosen and its channel operation executes, the race detector treats this as a synchronization event. (go.dev)- Race Detection within
select
Contexts: If aselect
statement's chosencase
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 theselect
'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)