[インデックス 14413] ファイルの概要
このコミットは、Go言語の標準ライブラリである sync/atomic パッケージ内の race.go ファイルに対する修正です。sync/atomic パッケージは、低レベルのアトミックなメモリ操作を提供し、複数のゴルーチンが共有データに安全にアクセスできるようにします。race.go ファイルは、Goのデータ競合検出器(Race Detector)がアトミック操作を正しく監視するためのインストゥルメンテーションコードを含んでいます。この修正は、特にポインタ型のアトミック操作におけるデータ競合検出の正確性を向上させることを目的としています。
コミット
sync/atomic パッケージにおけるデータ競合検出器のインストゥルメンテーションのバグを修正しました。具体的には、LoadUintptr、StorePointer、StoreUintptr 関数において、runtime.RaceRead 関数に渡す引数が誤って値(val)になっていたのを、アドレス(addr)に修正しました。これにより、データ競合検出器が正しいメモリ位置へのアクセスを監視できるようになり、誤検知や見逃しを防ぎます。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/27087022cea3b9fec19c9fae206abd57bf880bd3
元コミット内容
commit 27087022cea3b9fec19c9fae206abd57bf880bd3
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Nov 15 21:30:24 2012 +0400
sync/atomic: fix race instrumentation
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/6782075
変更の背景
Go言語には、並行処理におけるデータ競合(data race)を検出するための強力なツールである「Race Detector」が組み込まれています。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生します。データ競合はプログラムの予測不能な動作やクラッシュの原因となるため、その検出は非常に重要です。
sync/atomic パッケージは、ミューテックスなどのロック機構を使用せずに、アトミックな(不可分な)操作を提供することで、特定の共有変数へのアクセスを安全に行うことを可能にします。しかし、これらのアトミック操作自体も、Race Detectorによって正しく監視される必要があります。
このコミット以前は、sync/atomic パッケージ内の特定の関数(LoadUintptr, StorePointer, StoreUintptr)において、Race Detectorにアクセスを通知する runtime.RaceRead 関数に誤った引数が渡されていました。具体的には、メモリ位置の「アドレス」ではなく、そのメモリ位置から読み取られた「値」が渡されていました。これにより、Race Detectorは実際にアクセスが行われたメモリ位置を正確に特定できず、データ競合の誤検知や見逃しが発生する可能性がありました。この修正は、Race Detectorがアトミック操作を正しく追跡し、データ競合を正確に報告できるようにするために不可欠でした。
前提知識の解説
Goの sync/atomic パッケージ
sync/atomic パッケージは、Go言語でアトミックな操作を行うためのプリミティブを提供します。アトミック操作とは、複数のCPUコアやゴルーチンから同時にアクセスされた場合でも、その操作全体が中断されることなく単一の不可分な操作として実行されることを保証するものです。これにより、ロックを使用せずに共有変数を安全に更新できます。
例えば、atomic.AddInt32 は int32 型の変数にアトミックに値を加算します。
Goの Race Detector
GoのRace Detectorは、Goプログラムの実行中にデータ競合を検出するためのツールです。プログラムを -race フラグ付きでビルドして実行すると、Race Detectorが有効になり、メモリへのアクセスを監視します。データ競合が検出されると、Race Detectorは詳細なレポート(競合が発生した場所、スタックトレースなど)を出力し、開発者が問題を特定して修正するのに役立ちます。
unsafe.Pointer
unsafe.Pointer は、Goの型システムをバイパスして、任意の型のポインタを任意の型のポインタに変換できる特殊なポインタ型です。これは非常に強力ですが、誤用するとメモリ安全性や型安全性を損なう可能性があるため、「unsafe」と名付けられています。sync/atomic パッケージのような低レベルのコードでは、特定のメモリ操作のために unsafe.Pointer が使用されることがあります。
runtime パッケージとRace Detectorの内部関数
GoのRace Detectorは、runtime パッケージ内の特定の関数を呼び出すことで、メモリへのアクセスを監視します。これらの関数は通常、Goのユーザーコードから直接呼び出されることはなく、コンパイラによって生成されたコードや、sync/atomic のような低レベルのライブラリによって内部的に使用されます。
runtime.RaceSemacquire(addr *uintptr): セマフォを取得し、addrで示されるメモリ位置へのアクセスを同期します。runtime.RaceSemrelease(addr *uintptr): セマフォを解放し、addrで示されるメモリ位置へのアクセス同期を解除します。runtime.RaceRead(addr unsafe.Pointer):addrで示されるメモリ位置への読み取りアクセスをRace Detectorに通知します。runtime.RaceAcquire(addr unsafe.Pointer):addrで示されるメモリ位置への「取得(acquire)」操作をRace Detectorに通知します。これは、メモリバリアの一種で、この操作より前のメモリ書き込みが、この操作より後のメモリ読み取りから見えることを保証します。runtime.RaceRelease(addr unsafe.Pointer):addrで示されるメモリ位置への「解放(release)」操作をRace Detectorに通知します。これは、メモリバリアの一種で、この操作より前のメモリ書き込みが、この操作より後のメモリ読み取りから見えることを保証します。
これらの関数は、Race Detectorが並行アクセスパターンを追跡し、データ競合のルール(異なるゴルーチンからの非同期な読み書きアクセス)に違反しているかどうかを判断するために使用されます。
技術的詳細
このコミットの核心は、runtime.RaceRead 関数に渡される引数の修正です。
元のコードでは、LoadUintptr、StorePointer、StoreUintptr の各関数内で、runtime.RaceRead が以下のように呼び出されていました。
runtime.RaceRead(unsafe.Pointer(val)) // 誤り
ここで val は、LoadUintptr の場合は読み取られた値、StorePointer や StoreUintptr の場合は書き込まれる値です。
しかし、Race Detectorが監視すべきは「どのメモリ位置が読み取られたか/書き込まれたか」であり、そのメモリ位置に格納されている「値」ではありません。unsafe.Pointer(val) は、val のビットパターンをアドレスとして解釈しようとしますが、これは通常、有効なメモリ位置を指しません。
正しい動作のためには、runtime.RaceRead には操作対象のメモリ位置の「アドレス」を渡す必要があります。このアドレスは、関数の引数として渡される addr ポインタが指す場所です。
修正後のコードでは、この点が以下のように変更されました。
runtime.RaceRead(unsafe.Pointer(addr)) // 正しい
unsafe.Pointer(addr) は、addr が指すメモリ位置のアドレスを unsafe.Pointer 型に変換して runtime.RaceRead に渡します。これにより、Race Detectorは実際にアクセスが行われたメモリ位置を正確に識別し、そのメモリ位置に対する読み取り操作を正しく記録できるようになります。
この修正は、特にポインタ型(unsafe.Pointer や uintptr)をアトミックに操作する際に重要です。これらの型は、メモリ内の任意のアドレスを表現できるため、Race Detectorが正確なメモリ位置を追跡することが、データ競合の正確な検出に直結します。
コアとなるコードの変更箇所
--- a/src/pkg/sync/atomic/race.go
+++ b/src/pkg/sync/atomic/race.go
@@ -165,7 +165,7 @@ func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) {
func LoadUintptr(addr *uintptr) (val uintptr) {
runtime.RaceSemacquire(&mtx)
- runtime.RaceRead(unsafe.Pointer(val))
+ runtime.RaceRead(unsafe.Pointer(addr))
runtime.RaceAcquire(unsafe.Pointer(addr))
val = *addr
runtime.RaceSemrelease(&mtx)
@@ -198,7 +198,7 @@ func StoreUint64(addr *uint64, val uint64) {
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) {
runtime.RaceSemacquire(&mtx)
- runtime.RaceRead(unsafe.Pointer(val))
+ runtime.RaceRead(unsafe.Pointer(addr))
*addr = val
runtime.RaceRelease(unsafe.Pointer(addr))
runtime.RaceSemrelease(&mtx)
@@ -206,7 +206,7 @@ func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) {
func StoreUintptr(addr *uintptr, val uintptr) {
runtime.RaceSemacquire(&mtx)
- runtime.RaceRead(unsafe.Pointer(val))
+ runtime.RaceRead(unsafe.Pointer(addr))
*addr = val
runtime.RaceRelease(unsafe.Pointer(addr))
runtime.RaceSemrelease(&mtx)
コアとなるコードの解説
上記の差分は、src/pkg/sync/atomic/race.go ファイル内の3つの関数 LoadUintptr, StorePointer, StoreUintptr における変更を示しています。
各関数において、runtime.RaceSemacquire(&mtx) の直後にある runtime.RaceRead の呼び出しが修正されています。
-
変更前:
runtime.RaceRead(unsafe.Pointer(val))valは、LoadUintptrの場合は読み取られる値、StorePointerやStoreUintptrの場合は書き込まれる値です。unsafe.Pointer(val)は、valのビットパターンをメモリのアドレスとして解釈しようとします。これは、Race Detectorが監視すべき実際のメモリ位置とは異なります。
-
変更後:
runtime.RaceRead(unsafe.Pointer(addr))addrは、アトミック操作の対象となるメモリ位置へのポインタです(例:*uintptrや*unsafe.Pointer)。unsafe.Pointer(addr)は、addrが指す実際のメモリ位置のアドレスをunsafe.Pointer型に変換します。- これにより、
runtime.RaceReadは、実際に読み取りまたは書き込みが行われるメモリ位置を正確にRace Detectorに通知できるようになります。
この修正により、GoのRace Detectorは、sync/atomic パッケージを介したポインタ型のアトミック操作におけるデータ競合をより正確に検出できるようになりました。これは、並行プログラムのデバッグと信頼性向上において重要な改善です。
関連リンク
- Go言語の
sync/atomicパッケージ: https://pkg.go.dev/sync/atomic - GoのRace Detectorに関する公式ドキュメント: https://go.dev/doc/articles/race_detector
unsafe.Pointerのドキュメント: https://pkg.go.dev/unsafe#Pointer
参考にした情報源リンク
- Go CL 6782075: https://golang.org/cl/6782075
- Goのソースコード:
src/pkg/sync/atomic/race.go(Goリポジトリ内)