[インデックス 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
文におけるチャネルのclose
とsend
の間の競合を正確に検出できるように、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つの条件がすべて満たされたときに発生します。
- 少なくとも2つのゴルーチンが同じメモリ位置にアクセスする。
- 少なくとも1つのアクセスが書き込みである。
- アクセスが同期メカニズムによって保護されていない。
データ競合は、プログラムの非決定的な動作、クラッシュ、または誤った結果を引き起こす可能性があります。
Go Race Detector
Go Race Detectorは、Goプログラムの実行中にデータ競合を検出するためのツールです。プログラムをgo run -race
、go 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
の呼び出しが欠落していました。
この欠落により、以下のシナリオで競合が検出されませんでした。
- ゴルーチンAが
select
文内でチャネルc
への送信を試みる。 - ゴルーチンBがほぼ同時にチャネル
c
をclose
する。 - ゴルーチン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が有効な場合(
raceenabled
がtrue
)、チャネルc
のclosed
状態を読み取る直前に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
はテスト対象のチャネルです。c1
はselect
文の別のケースとして使用されるダミーチャネルです。- 最初のゴルーチンは、
time.Sleep(10 * time.Millisecond)
で少し待機した後、select
文内でc <- 1
を試みます。この遅延は、チャネルクローズ操作が先に実行される可能性を高め、競合状態を発生させやすくするためです。 - 2番目のゴルーチンは、
close(c)
を呼び出してチャネルc
をクローズします。 - 両方のゴルーチンが完了すると、
compl
チャネルにtrue
を送信します。 - メインゴルーチンは
<-compl
を2回受信することで、両方のゴルーチンの完了を待ちます。
このテストは、select
文内のチャネル送信とチャネルクローズの間の競合を意図的に引き起こし、Race Detectorがこれを検出できることを確認します。もしRace Detectorが競合を報告しない場合、テストは失敗します。
関連リンク
- Go言語のチャネル: https://go.dev/tour/concurrency/2
- Go言語の
select
文: https://go.dev/tour/concurrency/5 - Go Race Detector: https://go.dev/doc/articles/race_detector
参考にした情報源リンク
- Goの公式ドキュメント
- Goのソースコード(特に
src/runtime/chan.go
、src/runtime/select.go
、src/runtime/race/race.go
など) - GoのIssueトラッカーやメーリングリストでの関連議論 (今回のコミットのCLリンク:
https://golang.org/cl/10137043
) - データ競合に関する一般的な情報源