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

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

このコミットは、Goランタイムのデータ競合検出器(Race Detector)におけるチャネル同期のハンドリングをより正確にするためのものです。特に、キャパシティが1より大きい「逆チャネルセマフォ」パターンにおけるデータ競合の誤検出を防ぐことを目的としています。

コミット

commit 9e1cadad0f64698636d4dd7a3543619b3cb269a3
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Apr 8 10:18:20 2014 +0400

    runtime/race: more precise handling of channel synchronization
    It turns out there is a relatively common pattern that relies on
    inverted channel semaphore:

    gate := make(chan bool, N)
    for ... {
            // limit concurrency
            gate <- true
            go func() {
                    foo(...)
                    <-gate
            }()
    }
    // join all goroutines
    for i := 0; i < N; i++ {
            gate <- true
    }

    So handle synchronization on inverted semaphores with cap>1.
    Fixes #7718.

    LGTM=rsc
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/84880046
---\n src/pkg/runtime/chan.goc                   | 12 +++------
 src/pkg/runtime/race/testdata/chan_test.go | 41 ++++++++++++++++--------------
 2 files changed, 26 insertions(+), 27 deletions(-)

diff --git a/src/pkg/runtime/chan.goc b/src/pkg/runtime/chan.goc
index 185219640c..7a584717bb 100644
--- a/src/pkg/runtime/chan.goc
+++ b/src/pkg/runtime/chan.goc
@@ -172,8 +172,7 @@ asynch:
 	}

 	if(raceenabled) {
-\t\tif(c->dataqsiz == 1)\n-\t\t\truntime·raceacquire(chanbuf(c, c->sendx));
+\t\truntime·raceacquire(chanbuf(c, c->sendx));
 \t\truntime·racerelease(chanbuf(c, c->sendx));
 \t}\n 
@@ -304,8 +303,7 @@ asynch:

 	if(raceenabled) {
 \t\truntime·raceacquire(chanbuf(c, c->recvx));
-\t\tif(c->dataqsiz == 1)\n-\t\t\truntime·racerelease(chanbuf(c, c->recvx));
+\t\truntime·racerelease(chanbuf(c, c->recvx));
 \t}\n 

 \tif(ep != nil)\n@@ -855,8 +853,7 @@ asyncrecv:
 \t\tif(cas->sg.elem != nil)\n \t\t\truntime·racewriteobjectpc(cas->sg.elem, c->elemtype, cas->pc, chanrecv);\n \t\truntime·raceacquire(chanbuf(c, c->recvx));
-\t\tif(c->dataqsiz == 1)\n-\t\t\truntime·racerelease(chanbuf(c, c->recvx));
+\t\truntime·racerelease(chanbuf(c, c->recvx));
 \t}\n \tif(cas->receivedp != nil)\n \t\t*cas->receivedp = true;
@@ -881,8 +878,7 @@ asyncsend:
 	// can send to buffer
 \tif(raceenabled) {
-\t\tif(c->dataqsiz == 1)\n-\t\t\truntime·raceacquire(chanbuf(c, c->sendx));
+\t\truntime·raceacquire(chanbuf(c, c->sendx));
 \t\truntime·racerelease(chanbuf(c, c->sendx));
 \t\truntime·racereadobjectpc(cas->sg.elem, c->elemtype, cas->pc, chansend);\n \t}\ndiff --git a/src/pkg/runtime/race/testdata/chan_test.go b/src/pkg/runtime/race/testdata/chan_test.go
index aab59a553d..4a3d529022 100644
--- a/src/pkg/runtime/race/testdata/chan_test.go
+++ b/src/pkg/runtime/race/testdata/chan_test.go
@@ -567,22 +567,6 @@ func TestRaceChanCloseLen(t *testing.T) {
 	v = 2
 }

-func TestRaceChanSameCell(t *testing.T) {
-	c := make(chan int, 2)
-	v := 0
-	go func() {
-		v = 1
-		c <- 42
-		<-c
-		c <- 42
-		<-c
-	}()
-	time.Sleep(1e7)
-	c <- 43
-	<-c
-	_ = v
-}
-
 func TestRaceChanCloseSend(t *testing.T) {
 	compl := make(chan bool, 1)
 	c := make(chan int, 10)
@@ -641,16 +625,35 @@ func TestNoRaceSelectMutex(t *testing.T) {

 func TestRaceChanSem(t *testing.T) {
 	done := make(chan struct{})
-	mtx := make(chan struct{}, 2)
+	mtx := make(chan bool, 2)
 	data := 0
 	go func() {
-		mtx <- struct{}{}
+		mtx <- true
 		data = 42
 		<-mtx
 		done <- struct{}{}
 	}()
-	mtx <- struct{}{}
+	mtx <- true
 	data = 43
 	<-mtx
 	<-done
 }
+
+func TestNoRaceChanWaitGroup(t *testing.T) {
+	const N = 10
+	chanWg := make(chan bool, N/2)
+	data := make([]int, N)
+	for i := 0; i < N; i++ {
+		chanWg <- true
+		go func(i int) {
+			data[i] = 42
+			<-chanWg
+		}(i)
+	}
+	for i := 0; i < cap(chanWg); i++ {
+		chanWg <- true
+	}
+	for i := 0; i < N; i++ {
+		_ = data[i]
+	}
+}

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

https://github.com/golang/go/commit/9e1cadad0f64698636d4dd7a3543619b3cb269a3

元コミット内容

commit 9e1cadad0f64698636d4dd7a3543619b3cb269a3
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Apr 8 10:18:20 2014 +0400

    runtime/race: more precise handling of channel synchronization
    It turns out there is a relatively common pattern that relies on
    inverted channel semaphore:

    gate := make(chan bool, N)
    for ... {
            // limit concurrency
            gate <- true
            go func() {
                    foo(...)
                    <-gate
            }()
    }
    // join all goroutines
    for i := 0; i < N; i++ {
            gate <- true
    }

    So handle synchronization on inverted semaphores with cap>1.
    Fixes #7718.

    LGTM=rsc
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/84880046

変更の背景

Goのデータ競合検出器(Race Detector)は、並行処理におけるデータ競合、すなわち複数のゴルーチンが同時に同じメモリにアクセスし、少なくとも一方が書き込みである場合に、適切な同期なしに行われるアクセスを検出するための強力なツールです。しかし、特定のチャネル利用パターンにおいて、検出器が誤った競合を報告する可能性がありました。

このコミットの背景にある問題は、特に「逆チャネルセマフォ(inverted channel semaphore)」と呼ばれるパターンに起因します。通常のセマフォでは、チャネルへの送信(ch <- value)がリソースの取得、受信(<-ch)がリソースの解放に対応します。これにより、チャネルのバッファサイズが同時にアクセスできるリソースの数を制限します。

しかし、提示された「逆チャネルセマフォ」パターンでは、この役割が逆転しています。

gate := make(chan bool, N) // Nはバッファサイズ
for ... {
        // 実行中のゴルーチン数を制限
        gate <- true // リソース取得(バッファが埋まるまでブロック)
        go func() {
                foo(...)
                <-gate // リソース解放
        }()
}
// 全てのゴルーチンが完了するのを待つ
for i := 0; i < N; i++ {
        gate <- true // ここで全てのゴルーチンが完了するのを待つ
}

このパターンでは、gate <- true がゴルーチンの開始を許可し、<-gate がゴルーチンの終了を通知します。そして、最後に N 回の gate <- true を行うことで、全てのゴルーチンが終了するのを待ちます。この「待機」のメカニズムは、チャネルのバッファが全て埋まることで実現されます。

従来のGo Race Detectorは、チャネルのバッファサイズが1の場合(c->dataqsiz == 1)にのみ、チャネル操作が同期イベントとして機能すると仮定していました。これは、バッファサイズが1のチャネルがミューテックスのように振る舞うため、その送受信が明確な「happens-before」関係を確立すると考えられていたからです。しかし、上記の「逆チャネルセマフォ」のように、バッファサイズが1より大きいチャネルでも同期メカニズムとして機能するパターンが存在します。

この仮定の誤りにより、バッファサイズが1より大きいチャネルを用いた同期パターンにおいて、実際にはデータ競合が発生していないにもかかわらず、Race Detectorが誤って競合を報告する可能性がありました。このコミットは、この誤検出を修正し、Race Detectorの精度を向上させることを目的としています。

前提知識の解説

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

Go Race Detectorは、Goプログラムにおけるデータ競合を検出するためのツールです。データ競合は、以下の3つの条件が全て満たされたときに発生します。

  1. 少なくとも2つのゴルーチンが同時に同じメモリ位置にアクセスする。
  2. それらのアクセスの少なくとも1つが書き込みである。
  3. それらのアクセスが同期メカニズムによって順序付けられていない。

Race Detectorは、プログラムの実行中にメモリアクセスを監視し、これらの条件が満たされた場合に警告を発します。これは、GoogleのThreadSanitizer (TSan) ライブラリを基盤としており、コンパイル時に-raceフラグを付けることで有効になります(例: go run -race main.go)。有効化されると、コンパイラはメモリアクセス命令に計測コードを挿入し、ランタイムが各メモリアクセスのメタデータ(操作の種類、ゴルーチンID、タイムスタンプなど)を追跡します。

Race Detectorは「happens-before」メモリモデルに基づいて動作します。これは、特定の同期イベント(ミューテックスのロック/アンロック、チャネルの送受信など)が、メモリ操作の順序を保証するという考え方です。runtime·raceacquireruntime·racereleaseは、この「happens-before」関係をRace Detectorに通知するための内部ランタイム関数です。

  • runtime·raceacquire: ある操作がリソースを取得した、または以前の操作によって解放された状態を観測したことを示します。これにより、対応するracerelease操作より前に発生した全てのメモリ書き込みが、このraceacquire操作の後に可視になることが保証されます。これは、Cメモリモデルのatomic_load with memory_order_acquireに相当します。
  • runtime·racerelease: あるゴルーチンがそのメモリ書き込みを、同じアドレスで後続のacquire操作を実行する他のゴルーチンに可視にすることを示します。これは、Cメモリモデルのatomic_store with memory_order_releaseに相当します。

これらの関数は、ミューテックスやチャネルのような同期プリミティブの周りで、Goコンパイラによって暗黙的に呼び出され、Race Detectorに同期イベントを通知します。

Goのチャネルとセマフォ

Goのチャネルは、ゴルーチン間の通信と同期のための強力なプリミティブです。チャネルにはバッファの有無があり、バッファ付きチャネルはセマフォとして機能させることができます。

  • セマフォ: 共有リソースへのアクセスを制御するための並行処理プリミティブです。カウンタを保持し、同時にリソースにアクセスできるゴルーチンの数を制限します。
    • バイナリセマフォ: ミューテックスのように機能し、一度に1つのゴルーチンのみがアクセスを許可されます。
    • カウンティングセマフォ: 指定された数のゴルーチンが同時にリソースにアクセスすることを許可します。

Goでは、バッファ付きチャネルを使ってカウンティングセマフォを実装するのが一般的です。チャネルのキャパシティがセマフォの最大同時実行数となります。

  • セマフォの取得: バッファ付きチャネルに値を送信(ch <- value)します。チャネルが満杯の場合(最大同時実行数に達している場合)、送信操作はブロックされ、スロットが利用可能になるまで待機します。
  • セマフォの解放: バッファ付きチャネルから値を受信(<-ch)します。これにより、チャネルにスロットが解放され、他のゴルーチンがセマフォを取得できるようになります。

通常、セマフォとして使用する場合、送信される値自体は重要ではないため、メモリオーバーヘッドを最小限に抑えるために空のstruct{}がよく使用されます。

chan.goc

src/pkg/runtime/chan.gocは、Goランタイムのチャネル実装に関連するC言語のソースファイルです。Goランタイムの多くの部分はC言語で書かれており、GoのコードとCのコードが混在しています。このファイルは、チャネルの送受信、クローズなどの低レベルな操作を定義しており、Race Detectorが有効な場合には、これらの操作の際にruntime·raceacquireruntime·racereleaseといった関数が呼び出されるように計測コードが挿入されます。

技術的詳細

このコミットの核心は、Go Race Detectorがチャネル同期を扱う際の誤った仮定を修正することにあります。以前のRace Detectorの実装では、チャネルのバッファサイズ(c->dataqsiz)が1の場合にのみ、チャネルの送受信操作が同期イベントとしてruntime·raceacquireまたはruntime·racereleaseを呼び出す条件がありました。

具体的には、src/pkg/runtime/chan.goc内のチャネル送受信処理において、raceenabled(Race Detectorが有効な場合)のブロック内で以下のような条件分岐が存在していました。

if(c->dataqsiz == 1)
    runtime·raceacquire(chanbuf(c, c->sendx)); // または recvx
runtime·racerelease(chanbuf(c, c->sendx)); // または recvx

このコードは、「バッファサイズが1のチャネルでのみraceacquireを呼び出す」というロジックを示しています。これは、バッファサイズが1のチャネルがミューテックスのように機能し、厳密な順序付けを保証するという考えに基づいています。しかし、コミットメッセージで示された「逆チャネルセマフォ」パターンは、バッファサイズが1より大きいチャネルでも同期メカニズムとして機能します。このパターンでは、チャネルのバッファが満杯になることが同期点となり、複数のゴルーチンが同時にチャネルに送信することで、それらのゴルーチンが全て完了するのを待つことができます。

Race Detectorがc->dataqsiz == 1という条件に縛られていたため、バッファサイズが1より大きいチャネルを用いたこのような有効な同期パターンを正しく認識できず、結果として誤ったデータ競合の警告を発していました。

このコミットでは、このif(c->dataqsiz == 1)という条件を削除することで、Race Detectorがバッファサイズに関わらず、全てのチャネル送受信操作を同期イベントとして適切に処理するように変更しています。これにより、Race Detectorは「逆チャネルセマフォ」のようなパターンを正しく解釈し、誤検出を減らし、より正確なデータ競合検出が可能になります。

テストファイルsrc/pkg/runtime/race/testdata/chan_test.goの変更も重要です。TestRaceChanSameCellというテストが削除され、TestRaceChanSemが修正され、新たにTestNoRaceChanWaitGroupが追加されています。

  • TestRaceChanSameCellの削除: このテストは、バッファ付きチャネルの同じスロットへの複数回の送受信が競合として検出されることを意図していた可能性がありますが、今回の修正によりその前提が不要になったか、あるいはより適切なテストに置き換えられたと考えられます。
  • TestRaceChanSemの修正: mtx := make(chan struct{}, 2)mtx := make(chan bool, 2)に変更されています。これは、チャネルの要素型をstruct{}からboolに変更しただけで、機能的な意味合いは変わりませんが、テストの意図をより明確にするためか、あるいはGoの進化に伴う慣習的な変更である可能性があります。
  • TestNoRaceChanWaitGroupの追加: この新しいテストは、まさにコミットメッセージで説明されている「逆チャネルセマフォ」パターンを模倣しています。chanWg := make(chan bool, N/2)というバッファ付きチャネルを使用し、ゴルーチンがチャネルに送信して開始し、受信して終了するパターンをテストしています。このテストがTestNoRaceというプレフィックスを持つことから、このパターンがデータ競合を引き起こさないことをRace Detectorが正しく認識できるようになったことを検証しています。

これらの変更により、Go Race Detectorはチャネルを用いたより複雑な同期パターンも正確に理解し、開発者が並行処理のバグをより効果的に特定できるようになります。

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

src/pkg/runtime/chan.goc

このファイルでは、raceenabledブロック内のチャネル送受信処理において、runtime·raceacquireの呼び出しからif(c->dataqsiz == 1)という条件が削除されました。

変更前:

if(raceenabled) {
    if(c->dataqsiz == 1)
        runtime·raceacquire(chanbuf(c, c->sendx)); // または recvx
    runtime·racerelease(chanbuf(c, c->sendx)); // または recvx
}

変更後:

if(raceenabled) {
    runtime·raceacquire(chanbuf(c, c->sendx)); // または recvx
    runtime·racerelease(chanbuf(c, c->sendx)); // または recvx
}

この変更は、asynchラベル、asyncrecvラベル、asyncsendラベルの各セクションで行われています。

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

  • TestRaceChanSameCell関数が完全に削除されました。
  • TestRaceChanSem関数内で、mtx := make(chan struct{}, 2)mtx := make(chan bool, 2)に変更され、それに伴いチャネルへの送受信値もstruct{}{}からtrueに変更されました。
  • TestNoRaceChanWaitGroupという新しいテスト関数が追加されました。このテストは、コミットメッセージで説明されている「逆チャネルセマフォ」パターンを実装し、データ競合が発生しないことを検証します。

コアとなるコードの解説

src/pkg/runtime/chan.gocにおける変更は、Go Race Detectorがチャネルの同期イベントをどのように解釈するかという、その根幹に関わるものです。

以前のコードでは、if(c->dataqsiz == 1)という条件が存在したため、Race Detectorはバッファサイズが1のチャネル(つまり、ミューテックスのように振る舞うチャネル)の送受信のみを、厳密な「happens-before」関係を確立する同期イベントとして認識していました。runtime·raceacquireは、この「happens-before」関係の「取得」側をRace Detectorに通知する役割を担っています。

この条件が削除されたことにより、runtime·raceacquireはチャネルのバッファサイズに関わらず、全てのチャネル送受信操作で呼び出されるようになりました。これは、Race Detectorが、バッファサイズが1より大きいチャネルであっても、その送受信が並行処理における有効な同期メカニズムとして機能する場合があることを認識するようになったことを意味します。

特に、コミットメッセージで言及されている「逆チャネルセマフォ」パターンでは、バッファ付きチャネルの容量が満たされることが、複数のゴルーチンが特定の処理を完了したことを示す同期点となります。このパターンは、従来のRace Detectorのロジックでは正しく同期として認識されず、誤ってデータ競合が報告される可能性がありました。条件を削除することで、Race Detectorはこのようなパターンを正しく解釈し、誤検出を減らすことができます。

chan_test.goの変更は、この修正が正しく機能することを検証するためのものです。TestNoRaceChanWaitGroupの追加は、まさに「逆チャネルセマフォ」パターンがデータ競合を引き起こさないことを確認するためのものであり、Race Detectorの精度向上を実証しています。

この変更は、Goの並行処理モデルの柔軟性をRace Detectorがより深く理解し、開発者がより複雑な同期パターンを安心して使用できるようにするための重要な改善と言えます。

関連リンク

  • Go Change List: https://golang.org/cl/84880046
  • 関連するGo Issue #7718: (注: このIssueの詳細は、現在の公開情報からは直接特定できませんでした。コミットが2014年のものであり、古いIssueトラッカーや内部的なIssueである可能性があります。)

参考にした情報源リンク