[インデックス 14097] ファイルの概要
このコミットは、Go言語のデータ競合検出機能(Data Race Detector)の一部として、syscall
パッケージにおける変更を導入するものです。特に、Read()
およびWrite()
システムコールに対して粗粒度(coarse-grained)の同期メカニズムを追加し、データ競合の検出を可能にすることを目的としています。
コミット
commit cffbfaeb1819bfb6770848c4d57615f0ee1a46ba
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Oct 9 20:51:58 2012 +0400
race: syscall changes
This is a part of a bigger change that adds data race detection feature:
https://golang.org/cl/6456044
The purpose of this patch is to provide coarse-grained synchronization
between all Read() and Write() calls.
R=rsc, bradfitz, alex.brainman
CC=golang-dev
https://golang.org/cl/6610064
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cffbfaeb1819bfb6770848c4d57615f0ee1a46ba
元コミット内容
race: syscall changes
This is a part of a bigger change that adds data race detection feature:
https://golang.org/cl/6456044
The purpose of this patch is to provide coarse-grained synchronization
between all Read() and Write() calls.
変更の背景
このコミットは、Go言語にデータ競合検出機能(Data Race Detector)を導入する大規模な変更の一部です。データ競合は、複数のゴルーチンが共有変数に同時にアクセスし、そのうち少なくとも1つが書き込み操作である場合に、適切な同期なしで発生するバグの一種です。これらのバグは非決定論的であり、再現が困難なため、デバッグが非常に難しいことで知られています。
Goのデータ競合検出器は、コンパイル時または実行時にメモリアクセスを計測し、同期されていない共有変数へのアクセスを監視することで、これらの競合を特定します。このコミットの具体的な目的は、syscall
パッケージ内のRead()
およびWrite()
システムコール間で粗粒度の同期を提供することです。これにより、これらのI/O操作がデータ競合検出器によって適切に監視され、関連する競合が報告されるようになります。
前提知識の解説
データ競合 (Data Race)
データ競合は、並行プログラミングにおける一般的なバグの一種です。以下の3つの条件がすべて満たされたときに発生します。
- 複数のゴルーチン(またはスレッド)が同じメモリ位置にアクセスする。
- 少なくとも1つのアクセスが書き込み操作である。
- アクセスが同期メカニズム(ミューテックス、チャネルなど)によって保護されていない。
データ競合が発生すると、プログラムの動作が非決定論的になり、予期せぬ結果やクラッシュを引き起こす可能性があります。
Goのデータ競合検出器 (Go Data Race Detector)
Go言語には、データ競合を検出するための組み込みツールが用意されています。これは、go test -race
、go run -race
、go build -race
などのコマンドに-race
フラグを追加することで有効にできます。
- 動作原理: データ競合検出器は、プログラムのメモリアクセスを計測します。これは、C/C++のThreadSanitizerランタイムライブラリに基づいています。計測されたアクセスは、Goランタイムによって監視され、共有変数に対する同期されていない操作が検出されると、競合が報告されます。
- 利点: データ競合はデバッグが非常に困難ですが、検出器は競合が検出された際に明確なレポートを提供し、バグの特定を支援します。誤検知(false positives)は発生しないように設計されています。
- 考慮事項: 検出器を有効にすると、メモリ使用量が5〜10倍、実行時間が2〜20倍増加するなど、パフォーマンスにオーバーヘッドが生じます。そのため、本番環境での常時有効化は推奨されず、テスト時、特に並行処理を含むコードの単体テスト、結合テスト、負荷テスト中に使用するのが最適です。検出器は、実行中に実際にトリガーされた競合のみを検出するため、包括的なテストカバレッジが重要です。
syscall
パッケージ
syscall
パッケージは、Goプログラムがオペレーティングシステム(OS)のプリミティブなシステムコールに直接アクセスするための機能を提供します。ファイルI/O、ネットワーク通信、プロセス管理など、OSレベルの操作を行う際に使用されます。このパッケージは、OS固有のシステムコールを抽象化し、Goプログラムから利用できるようにします。
unsafe.Pointer
unsafe.Pointer
は、Go言語において型安全性をバイパスし、任意の型のポインタを表現できる特殊なポインタ型です。これにより、Goの型システムでは通常許可されないメモリ操作(例: 異なる型のポインタ間の変換、ポインタとuintptr
間の変換)が可能になります。データ競合検出器のような低レベルのメモリ操作を伴う機能では、unsafe.Pointer
が使用されることがあります。
技術的詳細
このコミットの主要な技術的変更点は、syscall
パッケージ内のRead()
およびWrite()
システムコールにデータ競合検出のための同期メカニズムを組み込むことです。これは、主に以下の要素によって実現されています。
-
race.go
とrace0.go
の導入:race.go
は、ビルドタグ+build race
が指定された場合にコンパイルされるファイルです。このファイルには、データ競合検出が有効な場合にのみ使用されるraceAcquire
とraceReleaseMerge
関数が定義されています。これらの関数は、runtime
パッケージのRaceAcquire
とRaceReleaseMerge
を呼び出します。また、raceenabled
定数がtrue
に設定されます。race0.go
は、ビルドタグ+build !race
が指定された場合(つまり、データ競合検出が無効な場合)にコンパイルされるファイルです。このファイルでは、raceAcquire
とraceReleaseMerge
関数は空の関数として定義され、raceenabled
定数はfalse
に設定されます。これにより、データ競合検出が無効なビルドでは、これらの同期呼び出しがコンパイル時に最適化されて削除され、パフォーマンスオーバーヘッドが発生しないようになっています。
-
ioSync
変数の導入:ioSync
というint64
型の変数が導入されました。この変数は、Read()
およびWrite()
操作間の同期の対象となる共有メモリ位置として機能します。unsafe.Pointer(&ioSync)
としてそのアドレスがraceAcquire
およびraceReleaseMerge
に渡されます。
-
Read()
およびWrite()
システムコールの変更:- 各OS固有の
syscall
パッケージ(darwin, freebsd, linux, netbsd, openbsd, plan9, windows)において、Read()
およびWrite()
システムコールの実装が変更されました。 Read()
関数では、システムコールが成功し、エラーが発生しなかった場合にraceAcquire(unsafe.Pointer(&ioSync))
が呼び出されます。これは、読み取り操作が完了した後に、ioSync
に対する「取得」操作をデータ競合検出器に通知します。Write()
関数では、システムコールが実行される前にraceReleaseMerge(unsafe.Pointer(&ioSync))
が呼び出されます。これは、書き込み操作が開始される前に、ioSync
に対する「解放」操作をデータ競合検出器に通知します。- これらの
raceAcquire
とraceReleaseMerge
の呼び出しは、ioSync
変数を介してRead()
とWrite()
操作の間に同期関係を確立します。これにより、データ競合検出器は、これらのI/O操作が並行して実行された場合に発生する可能性のある競合を検出できるようになります。
- 各OS固有の
-
システムコール関数のリネームとラッパーの導入:
- 多くのOS固有の
syscall
ファイルにおいて、既存のRead
およびWrite
関数がread
およびwrite
(小文字)にリネームされました。 - そして、新しい
Read
およびWrite
(大文字)のラッパー関数が導入され、その中でデータ競合検出のためのraceAcquire
やraceReleaseMerge
の呼び出しが行われ、その後、リネームされた小文字のread
やwrite
関数が呼び出される構造になっています。 - また、一部のOSでは、
read
やwrite
の低レベルなシステムコールラッパーがreadlen
やwritelen
といった名前に変更され、SYS_READ
やSYS_WRITE
といったシステムコール番号が明示的に割り当てられています。これは、Goのシステムコールバインディングの生成プロセス(zsyscall_*.go
ファイル群)と連携しています。
- 多くのOS固有の
これらの変更により、Goのデータ競合検出器は、ファイルディスクリプタを介したRead()
およびWrite()
操作における並行アクセスを監視し、潜在的なデータ競合を報告できるようになります。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルと関数は以下の通りです。
src/pkg/syscall/race.go
: 新規追加。データ競合検出が有効な場合のraceAcquire
とraceReleaseMerge
の実装。src/pkg/syscall/race0.go
: 新規追加。データ競合検出が無効な場合のraceAcquire
とraceReleaseMerge
のスタブ実装。src/pkg/syscall/exec_unix.go
:read
関数呼び出しがreadlen
に変更。src/pkg/syscall/syscall_darwin.go
:Read
とWrite
のsys
定義が小文字のread
とwrite
に変更。read
とwrite
の低レベルなsys
定義がreadlen
とwritelen
に変更され、SYS_READ
/SYS_WRITE
が明示的に指定。src/pkg/syscall/syscall_freebsd.go
:syscall_darwin.go
と同様の変更。src/pkg/syscall/syscall_linux.go
:syscall_darwin.go
と同様の変更。src/pkg/syscall/syscall_netbsd.go
:syscall_darwin.go
と同様の変更。src/pkg/syscall/syscall_openbsd.go
:syscall_darwin.go
と同様の変更。src/pkg/syscall/syscall_plan9.go
:Read
とWrite
関数にraceenabled
チェックとraceAcquire
/raceReleaseMerge
呼び出し、およびioSync
変数の追加。src/pkg/syscall/syscall_unix.go
:Read
とWrite
関数にraceenabled
チェックとraceAcquire
/raceReleaseMerge
呼び出し、およびioSync
変数の追加。src/pkg/syscall/syscall_windows.go
:Read
とWrite
関数にraceenabled
チェックとraceAcquire
/raceReleaseMerge
呼び出し、およびioSync
変数の追加。src/pkg/syscall/zsyscall_darwin_386.go
(および他のzsyscall_*.go
ファイル):Read
関数がread
にリネーム。Write
関数がwrite
にリネーム。- 低レベルな
read
関数がreadlen
にリネーム。 - 低レベルな
write
関数がwritelen
にリネーム。
コアとなるコードの解説
このコミットの核心は、syscall
パッケージにおけるRead
とWrite
操作にデータ競合検出のフックを組み込むことです。
race.go
とrace0.go
これらのファイルは、データ競合検出機能の有効/無効を切り替えるための条件付きコンパイルを可能にします。
src/pkg/syscall/race.go
(データ競合検出有効時):
// +build race
package syscall
import (
"runtime"
"unsafe"
)
const raceenabled = true
func raceAcquire(addr unsafe.Pointer) {
runtime.RaceAcquire(addr)
}
func raceReleaseMerge(addr unsafe.Pointer) {
runtime.RaceReleaseMerge(addr)
}
+build race
タグにより、go build -race
のように-race
フラグが指定された場合にのみコンパイルされます。raceenabled
定数がtrue
に設定されます。raceAcquire
とraceReleaseMerge
は、runtime
パッケージの対応する関数を呼び出します。これらは、特定のメモリ位置(addr
)に対するメモリ操作の同期イベントをデータ競合検出器に通知するために使用されます。RaceAcquire
はメモリの「取得」を、RaceReleaseMerge
はメモリの「解放とマージ」を意味します。
src/pkg/syscall/race0.go
(データ競合検出無効時):
// +build !race
package syscall
import (
"unsafe"
)
const raceenabled = false
func raceAcquire(addr unsafe.Pointer) {
}
func raceReleaseMerge(addr unsafe.Pointer) {
}
+build !race
タグにより、-race
フラグが指定されていない場合にコンパイルされます。raceenabled
定数がfalse
に設定されます。raceAcquire
とraceReleaseMerge
は空の関数として定義されます。これにより、データ競合検出が不要なビルドでは、これらの関数呼び出しがインライン化され、実質的にコードから削除されるため、実行時のオーバーヘッドがなくなります。
Read()
とWrite()
関数の変更例 (syscall_unix.go
の場合)
多くのOS固有のsyscall
ファイルで同様のパターンが適用されていますが、syscall_unix.go
の変更は代表的です。
変更前 (概念):
func Read(fd int, p []byte) (n int, err error) {
// システムコール呼び出し
// ...
}
func Write(fd int, p []byte) (n int, err error) {
// システムコール呼び出し
// ...
}
変更後:
func Read(fd int, p []byte) (n int, err error) {
n, err = read(fd, p) // 実際のシステムコールは小文字のreadが担当
if raceenabled && err == nil {
raceAcquire(unsafe.Pointer(&ioSync)) // 読み取り完了後にacquire
}
return
}
func Write(fd int, p []byte) (n int, err error) {
if raceenabled {
raceReleaseMerge(unsafe.Pointer(&ioSync)) // 書き込み前にrelease
}
return write(fd, p) // 実際のシステムコールは小文字のwriteが担当
}
var ioSync int64 // 同期対象となる共有変数
Read
とWrite
は、それぞれ実際のシステムコールを行う小文字のread
とwrite
関数を呼び出すラッパー関数になりました。raceenabled
がtrue
の場合(つまり、データ競合検出が有効な場合)にのみ、raceAcquire
またはraceReleaseMerge
が呼び出されます。ioSync
というint64
型の変数が導入され、この変数のアドレスがraceAcquire
とraceReleaseMerge
に渡されます。これは、Read
とWrite
操作がこのioSync
変数を介して同期されることをデータ競合検出器に示します。Write
操作の前にraceReleaseMerge
が呼び出されることで、書き込み操作がioSync
に対する「解放」イベントとして扱われます。Read
操作の後にraceAcquire
が呼び出されることで、読み取り操作がioSync
に対する「取得」イベントとして扱われます。
- これにより、
Read
とWrite
が並行して実行され、ioSync
に対するアクセスが適切に同期されていないとデータ競合検出器が判断した場合に、警告が発せられます。これは、ファイルディスクリプタを介したI/O操作における潜在的な競合を検出するために重要です。
zsyscall_*.go
ファイルの変更
zsyscall_*.go
ファイル群は、Goのツールチェーンによって自動生成されるシステムコールバインディングです。このコミットでは、これらのファイル内の関数名も変更されています。
例えば、src/pkg/syscall/zsyscall_darwin_386.go
では、以下のような変更が見られます。
変更前:
func Read(fd int, p []byte) (n int, err error) { ... }
func Write(fd int, p []byte) (n int, err error) { ... }
func read(fd int, buf *byte, nbuf int) (n int, err error) { ... } // 低レベルなread
func write(fd int, buf *byte, nbuf int) (n int, err error) { ... } // 低レベルなwrite
変更後:
func read(fd int, p []byte) (n int, err error) { ... } // 旧Readがreadに
func write(fd int, p []byte) (n int, err error) { ... } // 旧Writeがwriteに
func readlen(fd int, buf *byte, nbuf int) (n int, err error) { ... } = SYS_READ // 旧readがreadlenに
func writelen(fd int, buf *byte, nbuf int) (n int, err error) { ... } = SYS_WRITE // 旧writeがwritelenに
この変更は、Goのsyscall
パッケージの内部構造を整理し、データ競合検出のためのラッパー関数(大文字のRead
/Write
)と、実際のシステムコールを呼び出す低レベルな関数(小文字のread
/write
、さらにreadlen
/writelen
)を明確に区別するために行われました。
これらの変更全体として、Goのデータ競合検出器がsyscall
パッケージを介したI/O操作における並行アクセスを効果的に監視し、開発者が並行処理のバグを早期に発見できるようにするための基盤を構築しています。
関連リンク
- 元の変更セット (CL): https://golang.org/cl/6456044
- このコミットの変更セット (CL): https://golang.org/cl/6610064
参考にした情報源リンク
- Go Data Race Detector: https://go.dev/blog/race-detector
- Go Concurrency Patterns: Context (Uber Engineering Blog): https://www.uber.com/blog/go-concurrency-patterns-context/
- Go Race Detector: https://go.dev/doc/articles/race_detector.html
- How to use Go’s Race Detector: https://betterprogramming.pub/how-to-use-gos-race-detector-212212212212