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

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

このコミットは、Goランタイムにおけるチャネルのselect文とチャネルのclose操作の間で発生しうるデータ競合を検出し、修正するためのものです。具体的には、select文内でチャネルへの送信(CaseSend)が行われる際に、そのチャネルが同時に閉じられることによって発生する競合状態を、Goのデータ競合検出器(Race Detector)が適切に報告できるように改善しています。

コミット

commit 62747bde6c72ea6335a28daaf148a970b991987b
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Jun 10 22:58:04 2013 +0400

    runtime: catch races between channel close and channel send in select
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/10137043

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

https://github.com/golang/go/commit/62747bde6c72ea6335a28daaf148a970b991987b

元コミット内容

runtime: catch races between channel close and channel send in select

変更の背景

Goの並行処理モデルの根幹をなすチャネルは、ゴルーチン間の安全な通信を可能にするための重要なプリミティブです。しかし、並行処理においては、複数のゴルーチンが共有リソースに同時にアクセスしようとすると、データ競合(data race)という問題が発生する可能性があります。データ競合は、プログラムの予測不能な動作やクラッシュを引き起こす原因となります。

Goには、このようなデータ競合を検出するための強力なツールである「Race Detector」が組み込まれています。Race Detectorは、実行時にメモリへのアクセスを監視し、競合状態を特定します。

このコミット以前は、select文内でチャネルへの送信操作が行われる際に、そのチャネルが別のゴルーチンによって同時に閉じられた場合、この特定の競合状態がRace Detectorによって適切に検出されないという問題がありました。通常のチャネル送信(runtime·chansend)では既に競合検出のインストゥルメンテーション(runtime·racereadpc)が導入されていましたが、select文内の送信ケース(CaseSend)には同様の考慮が欠けていました。

この検出漏れは、開発者が潜在的なバグを見逃す可能性を意味し、Goプログラムの堅牢性を損なうものでした。したがって、select文におけるチャネルのclosesendの間の競合を正確に検出できるように、Race Detectorの機能を拡張する必要がありました。

前提知識の解説

Goのチャネル (Channels)

Goのチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは型付けされており、特定の型の値のみを送受信できます。チャネルにはバッファリングされていない(unbuffered)チャネルと、バッファリングされている(buffered)チャネルがあります。

  • 送信 (Send): ch <- value のようにしてチャネルに値を送信します。
  • 受信 (Receive): value := <-ch または <-ch のようにしてチャネルから値を受信します。
  • クローズ (Close): close(ch) のようにしてチャネルを閉じます。閉じられたチャネルへの送信はパニックを引き起こしますが、受信は可能で、チャネルが空になった後に受信するとゼロ値とfalseが返されます。

select

select文は、複数のチャネル操作を待機し、準備ができた最初の操作を実行するためのGoの制御構造です。select文は、非ブロッキングなチャネル操作やタイムアウトの実装によく使用されます。

select {
case <-ch1:
    // ch1 から受信
case ch2 <- value:
    // ch2 へ送信
case <-time.After(timeout):
    // タイムアウト
default:
    // どのケースも準備ができていない場合
}

データ競合 (Data Race)

データ競合は、以下の3つの条件がすべて満たされたときに発生します。

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

データ競合は、プログラムの非決定的な動作、クラッシュ、または誤った結果を引き起こす可能性があります。

Go Race Detector

Go Race Detectorは、Goプログラムの実行中にデータ競合を検出するためのツールです。プログラムをgo run -racego build -race、またはgo test -raceで実行すると有効になります。Race Detectorは、メモリへのアクセスを監視し、競合が検出された場合に詳細なレポート(競合が発生した場所、スタックトレースなど)を出力します。これは、並行処理のバグを見つける上で非常に強力なデバッグツールです。

runtime·racereadpc

runtime·racereadpcは、Goランタイム内部で使用される関数で、Race Detectorがメモリ読み込み操作を監視するために呼び出されます。同様にruntime·racewritepcは書き込み操作を監視します。これらの関数は、特定のメモリ位置へのアクセスが発生した際に、そのアクセスが競合状態を引き起こす可能性があるかどうかを判断するために必要な情報をRace Detectorに提供します。

技術的詳細

このコミットの核心は、select文内のチャネル送信ケース(CaseSend)において、チャネルが閉じられた際に発生するデータ競合をRace Detectorが検出できるようにすることです。

Goのランタイムは、チャネル操作を処理する際に、内部的にruntime·chansendのような関数を呼び出します。通常のruntime·chansendのパスでは、既にチャネルが閉じられているかどうかをチェックする前に、raceenabled(Race Detectorが有効であるかを示すフラグ)がtrueの場合にruntime·racereadpc(c, pc, runtime·chansend)を呼び出して、チャネルcへの読み込みアクセスをRace Detectorに通知していました。これは、チャネルのclosed状態を読み取る操作が、別のゴルーチンによるclose操作と競合する可能性があるためです。

しかし、select文の内部実装であるruntime·selectgo関数(またはそれに相当するロジック)では、CaseSendの処理において、チャネルが閉じられているかどうかのチェック(c->closed)の前にruntime·racereadpcの呼び出しが欠落していました。

この欠落により、以下のシナリオで競合が検出されませんでした。

  1. ゴルーチンAがselect文内でチャネルcへの送信を試みる。
  2. ゴルーチンBがほぼ同時にチャネルccloseする。
  3. ゴルーチンAがc->closedを読み取る際に、ゴルーチンBによるc->closedへの書き込み(close操作の一部)と競合する。

このコミットは、runtime·selectgo内のCaseSend処理において、c->closedを読み取る直前にruntime·racereadpcを明示的に呼び出すことで、この競合状態をRace Detectorが捕捉できるようにしました。これにより、select文を介したチャネル送信とチャネルクローズの間の競合が、他のチャネル操作と同様に適切に報告されるようになります。

また、テストケースTestRaceChanSendSelectCloseが追加され、この特定の競合シナリオがRace Detectorによって正しく検出されることを検証しています。このテストは、select文内でチャネル送信を試みるゴルーチンと、そのチャネルをクローズする別のゴルーチンを同時に実行し、Race Detectorが競合を報告するかどうかを確認します。

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

src/pkg/runtime/chan.c

--- a/src/pkg/runtime/chan.c
+++ b/src/pkg/runtime/chan.c
@@ -186,7 +186,6 @@ runtime·chansend(ChanType *t, Hchan *c, byte *ep, bool *pres, void *pc)
 	}
 
 	runtime·lock(c);
-	// TODO(dvyukov): add similar instrumentation to select.
 	if(raceenabled)
 		runtime·racereadpc(c, pc, runtime·chansend);
 	if(c->closed)
@@ -946,6 +945,8 @@ loop:
 			break;
 
 		case CaseSend:
+			if(raceenabled)
+				runtime·racereadpc(c, cas->pc, runtime·chansend);
 			if(c->closed)
 				goto sclose;
 			if(c->dataqsiz > 0) {

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

--- a/src/pkg/runtime/race/testdata/chan_test.go
+++ b/src/pkg/runtime/race/testdata/chan_test.go
@@ -311,12 +311,35 @@ func TestRaceChanSendClose(t *testing.T) {
 	go func() {
 		defer func() {
 			recover()
+			compl <- true
 		}()
 		c <- 1
+	}()
+	go func() {
+		time.Sleep(10 * time.Millisecond)
+		close(c)
 		compl <- true
 	}()
+	<-compl
+	<-compl
+}
+
+func TestRaceChanSendSelectClose(t *testing.T) {
+	compl := make(chan bool, 2)
+	c := make(chan int, 1)
+	c1 := make(chan int)
+	go func() {
+		defer func() {
+			recover()
+			compl <- true
+		}()
+		time.Sleep(10 * time.Millisecond)
+		select {
+		case c <- 1:
+		case <-c1:
+		}
+	}()
 	go func() {
-		time.Sleep(1e7)
 		close(c)
 		compl <- true
 	}()

コアとなるコードの解説

src/pkg/runtime/chan.c の変更

  • 削除されたコメント: // TODO(dvyukov): add similar instrumentation to select. このコメントは、select文にも同様の競合検出インストゥルメンテーションを追加する必要があることを示していました。今回のコミットでそのTODOが完了したため、削除されました。

  • CaseSend ブロックへの追加: runtime·selectgo関数内のloopラベルのCaseSendブロックに以下の行が追加されました。

    if(raceenabled)
        runtime·racereadpc(c, cas->pc, runtime·chansend);
    

    このコードは、Race Detectorが有効な場合(raceenabledtrue)、チャネルcclosed状態を読み取る直前にruntime·racereadpcを呼び出します。cas->pcは現在のプログラムカウンタ(呼び出し元のコード位置)を示し、runtime·chansendは関連する操作のコンテキストを提供します。これにより、select文内のチャネル送信が、別のゴルーチンによるチャネルクローズと競合した場合に、Race Detectorがその競合を正確に報告できるようになります。

src/pkg/runtime/race/testdata/chan_test.go の変更

  • TestRaceChanSendClose の修正: 既存のTestRaceChanSendCloseテスト関数が修正されました。元のテストでは、チャネル送信ゴルーチンがパニックから回復した後、compl <- trueを送信して完了を通知していましたが、チャネルをクローズするゴルーチンからの完了通知がありませんでした。修正後、チャネルをクローズするゴルーチンもcompl <- trueを送信するようになり、テストの完了条件が<-complを2回受信するように変更されました。これにより、テストのロジックがより堅牢になりました。

  • TestRaceChanSendSelectClose の新規追加: このコミットの主要なテスト変更点です。TestRaceChanSendSelectCloseという新しいテスト関数が追加されました。

    • complチャネルはテストの完了を待機するために使用されます。
    • cはテスト対象のチャネルです。
    • c1select文の別のケースとして使用されるダミーチャネルです。
    • 最初のゴルーチンは、time.Sleep(10 * time.Millisecond)で少し待機した後、select文内でc <- 1を試みます。この遅延は、チャネルクローズ操作が先に実行される可能性を高め、競合状態を発生させやすくするためです。
    • 2番目のゴルーチンは、close(c)を呼び出してチャネルcをクローズします。
    • 両方のゴルーチンが完了すると、complチャネルにtrueを送信します。
    • メインゴルーチンは<-complを2回受信することで、両方のゴルーチンの完了を待ちます。

このテストは、select文内のチャネル送信とチャネルクローズの間の競合を意図的に引き起こし、Race Detectorがこれを検出できることを確認します。もしRace Detectorが競合を報告しない場合、テストは失敗します。

関連リンク

参考にした情報源リンク

  • Goの公式ドキュメント
  • Goのソースコード(特にsrc/runtime/chan.gosrc/runtime/select.gosrc/runtime/race/race.goなど)
  • GoのIssueトラッカーやメーリングリストでの関連議論 (今回のコミットのCLリンク: https://golang.org/cl/10137043)
  • データ競合に関する一般的な情報源