[インデックス 17327] ファイルの概要
このコミットは、Go言語のnet
パッケージにおいて、Windows環境でのネットワークI/O操作(Read
およびWrite
)にGoのレース検出器(Race Detector)のためのアノテーションを追加するものです。これにより、ネットワーク操作におけるデータ競合の検出精度が向上します。具体的には、fd_windows.go
内のRead
およびWrite
メソッドにraceAcquire
とraceReleaseMerge
の呼び出しが追加され、レース検出器がこれらのI/O操作を同期イベントとして認識できるようになります。また、レース検出器が有効な場合と無効な場合で異なる実装を提供するrace.go
とrace0.go
が新規に作成されています。
コミット
commit 88ee849a8a1c4e3b63874fcbb8a5bb6eebeeb98b
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Aug 19 23:09:24 2013 +0400
net: annotate Read/Write for race detector
Fixes #6167.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/13052043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/88ee849a8a1c4e3b63874fcbb8a5bb6eebeeb98b
元コミット内容
net: annotate Read/Write for race detector
このコミットは、Goのnet
パッケージにおけるRead
およびWrite
操作にレース検出器のためのアノテーションを追加します。これにより、データ競合の検出が改善されます。Issue #6167を修正します。
変更の背景
Goのレース検出器は、並行プログラムにおけるデータ競合(data race)を検出するための強力なツールです。データ競合は、複数のゴルーチンが同時に共有メモリにアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生します。このような競合は、プログラムの予測不能な動作やバグの原因となります。
ネットワークI/O操作は、内部的に複雑な同期メカニズムを持つことが多く、特にOSレベルの非同期I/Oを使用する場合、Goのレース検出器が標準的な同期プリミティブ(ミューテックスやチャネルなど)だけでは検出できないような、より低レベルの競合が発生する可能性があります。
このコミットの背景には、Goのnet
パッケージ、特にWindows環境でのI/O操作において、レース検出器が適切にデータ競合を検出できないケースが存在したという問題(Issue #6167)があります。この問題に対処するため、Read
およびWrite
操作が実行される際に、レース検出器に対して明示的に同期イベントを通知するアノテーションを追加する必要がありました。これにより、レース検出器はこれらのI/O操作を「happens-before」関係の一部として考慮し、より正確な競合検出が可能になります。
前提知識の解説
Goのレース検出器 (Race Detector)
Goのレース検出器は、Go 1.1で導入された機能で、並行処理におけるデータ競合を検出するために設計されています。go run -race
、go build -race
、go test -race
などのコマンドで有効にできます。レース検出器は、プログラムの実行時にメモリアクセスを監視し、同期されていない共有メモリへのアクセスを特定します。
データ競合 (Data Race)
データ競合は、以下の3つの条件がすべて満たされたときに発生します。
- 少なくとも2つのゴルーチンが同じメモリ位置にアクセスする。
- 少なくとも1つのアクセスが書き込みである。
- それらのアクセスが同期メカニズム(ミューテックス、チャネルなど)によって保護されていない。
データ競合は、プログラムの動作を非決定的にし、デバッグが困難なバグを引き起こす可能性があります。
Happens-Before 関係
並行プログラミングにおいて、「happens-before」関係は、イベント間の順序付けを定義する概念です。あるイベントAがイベントBの前に「happens-before」する場合、Aの結果はBから観測可能であることが保証されます。レース検出器は、このhappens-before関係を利用して、同期されていないメモリアクセスを特定します。同期プリミティブ(ミューテックスのロック/アンロック、チャネルの送受信など)は、このhappens-before関係を確立します。
runtime.RaceAcquire
と runtime.RaceReleaseMerge
これらはGoランタイムの内部関数であり、レース検出器に対して明示的に同期イベントを通知するために使用されます。
runtime.RaceAcquire(addr unsafe.Pointer)
: この関数は「acquire」操作を確立します。これは、指定されたアドレスaddr
に対する以前のRaceRelease
またはRaceReleaseMerge
操作の後に発生したすべてのメモリ書き込みが、このRaceAcquire
を実行するゴルーチンから見えるようになることをレース検出器に通知します。C11メモリモデルのatomic_load
withmemory_order_acquire
に似ています。runtime.RaceRelease(addr unsafe.Pointer)
: この関数は「release」操作を実行します。これは、このRaceRelease
呼び出しの前に現在のゴルーチンによってaddr
に対して行われたすべてのメモリ書き込みが、同じaddr
に対して後続のRaceAcquire
を実行する他のゴルーチンから見えるようになることをレース検出器に通知します。C11メモリモデルのatomic_store
withmemory_order_release
に似ています。runtime.RaceReleaseMerge(addr unsafe.Pointer)
:RaceRelease
に似ていますが、happens-before関係の「マージ」を意味します。これは、複数のリリース操作が後続のacquire操作のために論理的に結合される必要があるシナリオで使用されます。RaceAcquire
は、addr
に対する以前のRaceReleaseMerge
、およびaddr
に対する最後のRaceRelease
を含む、happens-before関係を確立します。
これらの関数は、Goの標準的な同期プリミティブでは検出できないような、低レベルのカスタム同期ポイントについてレース検出器に手動で通知するメカニズムを提供します。
技術的詳細
このコミットの主要な目的は、Windows環境におけるnet
パッケージのI/O操作(Read
とWrite
)が、レース検出器によって適切に同期イベントとして扱われるようにすることです。
Goのレース検出器は、通常、ミューテックスやチャネルなどの高レベルの同期プリミティブを監視することでデータ競合を検出します。しかし、OSの非同期I/Oメカニズム(WindowsのCompletion Portsなど)を使用する低レベルのネットワーク操作では、これらの高レベルのプリミティブが直接関与しない場合があります。その結果、I/Oバッファへのアクセスが複数のゴルーチン間で競合しても、レース検出器がそれを検出できない可能性があります。
この問題を解決するために、コミットではruntime.RaceAcquire
とruntime.RaceReleaseMerge
というランタイム関数が利用されています。これらの関数は、レース検出器に対して、特定のメモリ位置(この場合はioSync
というグローバル変数)に対するアクセスが同期イベントであることを明示的に伝えます。
具体的には、以下のロジックが導入されています。
ioSync
変数の導入:src/pkg/net/fd_windows.go
にioSync uint64
というグローバル変数が追加されました。この変数は、実際のデータではなく、レース検出器がI/O操作の同期を追跡するための「同期オブジェクト」として機能します。Read
操作でのraceAcquire
:fd_windows.go
のRead
メソッド内で、読み込みが完了した後にraceAcquire(unsafe.Pointer(&ioSync))
が呼び出されます。これは、Read
操作が完了した時点で、その操作によって読み込まれたデータが他のゴルーチンから安全にアクセス可能になることをレース検出器に通知します。つまり、Read
操作は、以前のWrite
操作によって「リリース」されたデータを「取得」する操作として扱われます。Write
操作でのraceReleaseMerge
:fd_windows.go
のWrite
メソッド内で、書き込みが完了する直前にraceReleaseMerge(unsafe.Pointer(&ioSync))
が呼び出されます。これは、Write
操作によってバッファに書き込まれたデータが、他のゴルーチンがRead
操作を通じて「取得」できるようになることをレース検出器に通知します。raceReleaseMerge
が使用されるのは、複数の書き込み操作が論理的に結合され、後続の読み込み操作によって取得される可能性があるためです。- レース検出器の有効/無効に応じた実装:
src/pkg/net/race.go
は、ビルドタグ+build race
が指定されている場合にコンパイルされます。このファイルでは、raceenabled
がtrue
に設定され、raceAcquire
、raceReleaseMerge
、raceReadRange
、raceWriteRange
がそれぞれruntime
パッケージの対応する関数を呼び出すように実装されています。src/pkg/net/race0.go
は、ビルドタグ+build !race
が指定されている場合にコンパイルされます。このファイルでは、raceenabled
がfalse
に設定され、すべてのrace*
関数が空の関数として実装されています。これにより、レース検出器が無効なビルドでは、これらのアノテーションによるオーバーヘッドがなくなります。
この変更により、Goのレース検出器は、ネットワークI/O操作におけるバッファへのアクセスを、明示的な同期イベントとして認識できるようになります。これにより、I/Oバッファの再利用や並行アクセスに関連するデータ競合が、より確実に検出されるようになります。
コアとなるコードの変更箇所
このコミットでは、以下の3つのファイルが変更または新規作成されています。
-
src/pkg/net/fd_windows.go
:ioSync
というuint64
型のグローバル変数が追加されました。Read
メソッドの戻り値の処理部分に、raceenabled
がtrue
の場合にraceAcquire(unsafe.Pointer(&ioSync))
を呼び出すコードが追加されました。Write
メソッドのdefer fd.writeUnlock()
の直後に、raceenabled
がtrue
の場合にraceReleaseMerge(unsafe.Pointer(&ioSync))
を呼び出すコードが追加されました。
-
src/pkg/net/race.go
(新規作成):+build race
および+build windows
ビルドタグを持つ新規ファイル。raceenabled
定数をtrue
に設定。runtime.RaceAcquire
、runtime.RaceReleaseMerge
、runtime.RaceReadRange
、runtime.RaceWriteRange
をラップするraceAcquire
、raceReleaseMerge
、raceReadRange
、raceWriteRange
関数を定義。
-
src/pkg/net/race0.go
(新規作成):+build !race
および+build windows
ビルドタグを持つ新規ファイル。raceenabled
定数をfalse
に設定。- すべての
race*
関数(raceAcquire
、raceReleaseMerge
、raceReadRange
、raceWriteRange
)を空の関数として定義。
コアとなるコードの解説
src/pkg/net/fd_windows.go
var (
initErr error
ioSync uint64 // レース検出器のための同期オブジェクト
)
// ...
func (fd *netFD) Read(buf []byte) (int, error) {
// ... 既存のRead処理 ...
if err == nil && n == 0 {
err = io.EOF
}
if raceenabled { // レース検出器が有効な場合
raceAcquire(unsafe.Pointer(&ioSync)) // 読み込み完了時にacquire操作を通知
}
return n, err
}
// ...
func (fd *netFD) Write(buf []byte) (int, error) {
// ... 既存のWrite処理 ...
defer fd.writeUnlock()
if raceenabled { // レース検出器が有効な場合
raceReleaseMerge(unsafe.Pointer(&ioSync)) // 書き込み完了時にreleaseMerge操作を通知
}
o := &fd.wop
o.InitBuf(buf)
return iosrv.ExecIO(o, "WSASend", func(o *operation) error {
// ...
})
}
ioSync
変数は、Read
とWrite
操作間の同期ポイントとしてレース検出器に認識させるためのダミーのアドレスとして機能します。Read
ではraceAcquire
が呼び出され、これはI/Oバッファへの読み込みが完了し、そのデータが他のゴルーチンから安全に「取得」できるようになったことを示します。Write
ではraceReleaseMerge
が呼び出され、これはI/Oバッファへの書き込みが完了し、そのデータが他のゴルーチンによって「取得」可能になったことを示します。raceReleaseMerge
は、複数の書き込みが論理的にマージされるシナリオに適しています。
src/pkg/net/race.go
// +build race
// +build windows
package net
import (
"runtime"
"unsafe"
)
const raceenabled = true // レース検出器が有効
func raceAcquire(addr unsafe.Pointer) {
runtime.RaceAcquire(addr) // ランタイムのRaceAcquireを呼び出す
}
func raceReleaseMerge(addr unsafe.Pointer) {
runtime.RaceReleaseMerge(addr) // ランタイムのRaceReleaseMergeを呼び出す
}
func raceReadRange(addr unsafe.Pointer, len int) {
runtime.RaceReadRange(addr, len)
}
func raceWriteRange(addr unsafe.Pointer, len int) {
runtime.RaceWriteRange(addr, len)
}
このファイルは、go build -race
のようにレース検出器が有効なビルド時にコンパイルされます。raceenabled
がtrue
に設定され、raceAcquire
やraceReleaseMerge
といった関数が、実際にGoランタイムの対応するレース検出器API(runtime.RaceAcquire
など)を呼び出すように実装されています。これにより、レース検出器はI/O操作におけるメモリアクセスの同期を正確に追跡できます。
src/pkg/net/race0.go
// +build !race
// +build windows
package net
import (
"unsafe"
)
const raceenabled = false // レース検出器が無効
func raceAcquire(addr unsafe.Pointer) {
// 何もしない
}
func raceReleaseMerge(addr unsafe.Pointer) {
// 何もしない
}
func raceReadRange(addr unsafe.Pointer, len int) {
// 何もしない
}
func raceWriteRange(addr unsafe.Pointer, len int) {
// 何もしない
}
このファイルは、レース検出器が無効なビルド時にコンパイルされます。raceenabled
がfalse
に設定され、すべてのrace*
関数が空の関数として実装されています。これにより、レース検出器が不要な場合には、これらのアノテーションによるパフォーマンスオーバーヘッドが完全に排除されます。これは、Goのビルドタグシステムを活用した典型的な条件付きコンパイルの例です。
関連リンク
- Go CL 13052043: https://golang.org/cl/13052043
- Go Issue 6167: https://github.com/golang/go/issues/6167
参考にした情報源リンク
- Go Race Detector Documentation: https://go.dev/doc/articles/race_detector
- Go Memory Model: https://go.dev/ref/mem
runtime.RaceAcquire
andruntime.RaceReleaseMerge
explanation: https://go.dev/src/runtime/race.go (Go source code)- ThreadSanitizer (TSan) overview: https://github.com/google/sanitizers/wiki/ThreadSanitizerCppDynamicAnnotations (Goのレース検出器の基盤技術)