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

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

コミット

commit 51e89f59b24d91829184ed0f48a82471c7ebb366
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Nov 14 16:51:23 2012 +0400

    runtime: add RaceRead/RaceWrite functions
    It allows to catch e.g. a data race between atomic write and non-atomic write,
    or Mutex.Lock() and mutex overwrite (e.g. mu = Mutex{}).
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6817103

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

https://github.com/golang/go/commit/51e89f59b24d91829184ed0f48a82471c7ebb366

元コミット内容

runtime: add RaceRead/RaceWrite functions It allows to catch e.g. a data race between atomic write and non-atomic write, or Mutex.Lock() and mutex overwrite (e.g. mu = Mutex{}).

変更の背景

このコミットは、Go言語のランタイムにRaceReadおよびRaceWrite関数を追加することを目的としています。これらの関数は、Goのデータ競合検出器(Race Detector)の機能を強化し、これまで検出が困難だった特定の種類のデータ競合を捕捉できるようにするために導入されました。具体的には、アトミックな書き込みと非アトミックな書き込みの間の競合、あるいはsync.MutexLock()操作とミューテックスの誤った上書き(例: mu = Mutex{}のようなゼロ値による初期化)といったケースを検出できるようになります。

データ競合は並行プログラミングにおける一般的なバグであり、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生します。このような競合はプログラムの予測不能な動作やクラッシュを引き起こす可能性があります。Goのデータ競合検出器は、実行時にこれらの競合を特定し、開発者に警告することで、並行プログラムのデバッグを支援します。このコミットは、その検出能力を向上させるための重要なステップです。

前提知識の解説

データ競合 (Data Race)

データ競合とは、複数の並行実行されるスレッド(Goにおいてはゴルーチン)が、同期メカニズムなしに同じメモリ位置にアクセスし、そのうち少なくとも1つのアクセスが書き込み操作である場合に発生するプログラミング上のバグです。データ競合が発生すると、プログラムの実行結果がアクセス順序に依存するようになり、非決定的な動作や予期せぬ結果(クラッシュ、不正なデータ、セキュリティ脆弱性など)を引き起こす可能性があります。

Goのデータ競合検出器 (Go Race Detector)

Go言語には、プログラム実行中にデータ競合を検出するための組み込みツールである「データ競合検出器」があります。これは、go run -racego build -racego test -raceなどのコマンドで有効にできます。データ競合検出器は、メモリへのアクセスを監視し、競合のパターンを特定することで、データ競合が発生した場所と状況を報告します。これにより、開発者は並行処理のバグを効率的に特定し、修正することができます。

アトミック操作 (Atomic Operations)

アトミック操作とは、不可分(atomic)に実行される操作のことです。つまり、その操作が開始されてから完了するまでの間に、他のスレッドやゴルーチンによって中断されることがありません。これにより、複数のゴルーチンが同時に同じメモリ位置にアクセスしても、データ競合が発生しないことが保証されます。Go言語では、sync/atomicパッケージがAddInt32, CompareAndSwapUint64, LoadPointer, StoreInt32などのアトミック操作を提供しています。これらは、ミューテックスのようなロックメカニズムを使用せずに、共有データへの安全なアクセスを提供するために使用されます。

unsafe.Pointer

unsafe.Pointerは、Go言語において任意の型のポインタを保持できる特殊な型です。これはC言語のvoid*に似ており、型安全性をバイパスしてメモリを直接操作することを可能にします。unsafeパッケージを使用すると、Goの型システムが提供する安全保証の一部を放棄することになるため、非常に注意して使用する必要があります。データ競合検出器のような低レベルのランタイム機能では、メモリアドレスを直接扱うためにunsafe.Pointerが頻繁に利用されます。

runtimeパッケージ

runtimeパッケージは、Goランタイムシステムとのインタフェースを提供します。ガベージコレクション、ゴルーチン管理、スケジューリング、低レベルのメモリ操作など、Goプログラムの実行環境に関する機能が含まれています。データ競合検出器の内部実装もこのパッケージの一部として提供されており、メモリへのアクセスをフックして競合を監視する機能がここに実装されています。

sync/atomicパッケージ

sync/atomicパッケージは、アトミックなプリミティブ操作を提供します。これらは、ミューテックスなどのロックを使用せずに、共有変数への並行アクセスを安全に行うための低レベルな操作です。このパッケージの関数は、CPUのハードウェアレベルのアトミック命令を利用して実装されており、非常に高速です。

技術的詳細

このコミットの主要な目的は、Goのデータ競合検出器の精度とカバレッジを向上させることです。これまでのデータ競合検出器は、一般的な非同期アクセス間の競合を検出できましたが、アトミック操作と非アトミック操作の間の競合や、ミューテックスの誤った使用による競合など、特定の微妙なケースを見逃す可能性がありました。

RaceReadRaceWriteの導入

このコミットでは、runtimeパッケージにRaceRead(addr unsafe.Pointer)RaceWrite(addr unsafe.Pointer)という2つの新しいC関数が追加されました。これらの関数は、指定されたメモリアドレスaddrに対する読み取りまたは書き込み操作をデータ競合検出器に明示的に通知するために使用されます。

  • runtime·RaceRead(void *addr): addrで示されるメモリ位置への読み取りアクセスを検出器に報告します。
  • runtime·RaceWrite(void *addr): addrで示されるメモリ位置への書き込みアクセスを検出器に報告します。

これらの関数は、Goのソースコードからは直接呼び出されるのではなく、主にsync/atomicパッケージのような低レベルのライブラリや、将来的にコンパイラによって生成されるコードから利用されることを意図しています。

sync/atomicパッケージへの統合

このコミットの重要な変更点の一つは、sync/atomicパッケージ内のアトミック操作のGo実装にruntime.RaceRead()の呼び出しが追加されたことです。具体的には、CompareAndSwap*関数やAdd*関数、Load*関数、Store*関数など、多くのアトミック操作の内部で、対象となるメモリアドレスに対してruntime.RaceRead(unsafe.Pointer(val))が呼び出されるようになりました。

この変更の背後にあるロジックは、コミットメッセージのコメントで詳しく説明されています。

// We use runtime.RaceRead() inside of atomic operations to catch races
// between atomic and non-atomic operations.  It will also catch races
// between Mutex.Lock() and mutex overwrite (mu = Mutex{}).  Since we use
// only RaceRead() we won't catch races with non-atomic loads.
// Otherwise (if we use RaceWrite()) we will report races
// between atomic operations (false positives).

このコメントは、以下の重要な点を説明しています。

  1. アトミック操作と非アトミック操作間の競合検出: RaceRead()をアトミック操作の内部で使用することで、アトミックな書き込みと非アトミックな書き込みの間の競合を検出できるようになります。例えば、あるゴルーチンがアトミックに変数に書き込み、別のゴルーチンがその変数に非アトミックに書き込む場合、これはデータ競合です。RaceRead()は、アトミック操作がそのメモリ位置を「読み取っている」かのように検出器に振る舞わせることで、このような競合を捕捉します。
  2. ミューテックスの上書き検出: Mutex.Lock()とミューテックスの誤った上書き(例: mu = Mutex{})の間の競合も検出できるようになります。Mutex{}はミューテックスのゼロ値であり、既にロックされているミューテックスをゼロ値で上書きすると、そのミューテックスの状態がリセットされ、ロックが解除されたかのように振る舞う可能性があります。これにより、複数のゴルーチンが同時にロックを取得しようとしてデータ競合を引き起こす可能性があります。RaceRead()は、ミューテックスの内部状態へのアクセスを監視することで、このような不正な上書きを検出するのに役立ちます。
  3. RaceRead()のみを使用する理由: sync/atomicパッケージではRaceWrite()ではなくRaceRead()のみが使用されています。これは、RaceWrite()を使用すると、アトミック操作間の競合(これらは本来安全であるべき)を誤ってデータ競合として報告してしまう「偽陽性(false positives)」が発生する可能性があるためです。アトミック操作はそれ自体が競合フリーであることを保証するように設計されているため、それらの間の競合を報告することは望ましくありません。RaceRead()を使用することで、アトミック操作の内部動作を「読み取り」としてマークし、非アトミックなアクセスとの競合のみを効果的に検出します。

getcallerpcの使用

runtime·RaceReadruntime·RaceWriteのC実装では、runtime·racereadpcruntime·racewritepcという内部関数が呼び出されています。これらの関数には、メモリアドレスに加えて、runtime·getcallerpc(&addr)によって取得される呼び出し元のプログラムカウンタ(PC)が渡されます。プログラムカウンタは、データ競合が発生したコードの正確な位置を特定するためにデータ競合検出器によって使用されます。これにより、競合レポートがより詳細で役立つものになります。

textflag 7

#pragma textflag 7は、Goのコンパイラ(gc)に対するディレクティブで、特定の関数がインライン化されないように指示します。データ競合検出器のフック関数は、正確なスタックトレースと呼び出し元の情報を取得するために、インライン化されないことが重要です。これにより、競合が発生した正確なコンテキストを特定できます。

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

このコミットでは、主に以下の3つのファイルが変更されています。

  1. src/pkg/runtime/race.c:

    • runtime·RaceRead(void *addr)関数の追加。内部でruntime·racereadpc(addr, runtime·getcallerpc(&addr))を呼び出す。
    • runtime·RaceWrite(void *addr)関数の追加。内部でruntime·racewritepc(addr, runtime·getcallerpc(&addr))を呼び出す。
    • 両関数に#pragma textflag 7ディレクティブを追加。
  2. src/pkg/runtime/race.go:

    • RaceRead(addr unsafe.Pointer)関数のGo宣言を追加。
    • RaceWrite(addr unsafe.Pointer)関数のGo宣言を追加。
    • これらの宣言は、Cで実装された関数をGoコードから呼び出すためのGo側のシグネチャを提供します。
  3. src/pkg/sync/atomic/race.go:

    • CompareAndSwapInt32, CompareAndSwapUint32, CompareAndSwapInt64, CompareAndSwapUint64, CompareAndSwapPointer, CompareAndSwapUintptrなどのCompareAndSwap系関数に、runtime.RaceRead(unsafe.Pointer(val))の呼び出しを追加。
    • AddInt32, AddUint32, AddInt64, AddUint64, AddUintptrなどのAdd系関数に、runtime.RaceRead(unsafe.Pointer(val))の呼び出しを追加。
    • LoadInt32, LoadUint32, LoadInt64, LoadUint64, LoadPointer, LoadUintptrなどのLoad系関数に、runtime.RaceRead(unsafe.Pointer(addr))の呼び出しを追加。
    • StoreInt32, StoreUint32, StoreInt64, StoreUint64, StorePointer, StoreUintptrなどのStore系関数に、runtime.RaceRead(unsafe.Pointer(addr))またはruntime.RaceRead(unsafe.Pointer(val))の呼び出しを追加。
    • これらの変更により、sync/atomicパッケージの各アトミック操作が、その操作対象のメモリ位置に対して「読み取り」アクセスがあったことをデータ競合検出器に報告するようになります。

コアとなるコードの解説

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

// func RaceRead(addr unsafe.Pointer)
#pragma textflag 7
void
runtime·RaceRead(void *addr)
{
	runtime·racereadpc(addr, runtime·getcallerpc(&addr));
}

// func RaceWrite(addr unsafe.Pointer)
#pragma textflag 7
void
runtime·RaceWrite(void *addr)
{
	runtime·racewritepc(addr, runtime·getcallerpc(&addr));
}

このCコードは、Goのruntimeパッケージから呼び出されるRaceReadRaceWriteの実装です。

  • #pragma textflag 7: このディレクティブは、コンパイラに対してこれらの関数をインライン化しないように指示します。データ競合検出器が正確な呼び出し元情報を取得するために重要です。
  • runtime·racereadpc / runtime·racewritepc: これらはデータ競合検出器の内部関数であり、指定されたメモリアドレス(addr)と呼び出し元のプログラムカウンタ(runtime·getcallerpc(&addr))を引数として受け取ります。これにより、検出器はメモリアクセスを記録し、潜在的な競合を分析します。

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

func RaceRead(addr unsafe.Pointer)
func RaceWrite(addr unsafe.Pointer)

これはGoの関数宣言であり、src/pkg/runtime/race.cで定義されたC関数に対応するGo側のシグネチャを提供します。Goコードからこれらの関数を呼び出す際には、この宣言が使用されます。

src/pkg/sync/atomic/race.goの変更

このファイルでは、sync/atomicパッケージ内のすべてのアトミック操作(CompareAndSwap*, Add*, Load*, Store*)にruntime.RaceRead()の呼び出しが追加されています。以下に例を示します。

CompareAndSwapUint32の変更前と変更後:

--- a/src/pkg/sync/atomic/race.go
+++ b/src/pkg/sync/atomic/race.go
@@ -20,6 +27,7 @@ func CompareAndSwapInt32(val *int32, old, new int32) bool {
 func CompareAndSwapUint32(val *uint32, old, new uint32) (swapped bool) {
 	swapped = false
 	runtime.RaceSemacquire(&mtx)
+	runtime.RaceRead(unsafe.Pointer(val)) // 追加された行
 	runtime.RaceAcquire(unsafe.Pointer(val))
 	if *val == old {
 		*val = new

この変更により、CompareAndSwapUint32が実行される際に、valが指すメモリ位置に対してruntime.RaceReadが呼び出されます。これは、アトミック操作がそのメモリ位置を「読み取っている」ことをデータ競合検出器に通知します。これにより、アトミック操作と非アトミックな書き込み操作との間の競合が検出可能になります。

LoadUint32の変更前と変更後:

--- a/src/pkg/sync/atomic/race.go
+++ b/src/pkg/sync/atomic/race.go
@@ -120,6 +134,7 @@ func LoadInt32(addr *int32) int32 {
 func LoadUint32(addr *uint32) (val uint32) {
 	runtime.RaceSemacquire(&mtx)
+	runtime.RaceRead(unsafe.Pointer(addr)) // 追加された行
 	runtime.RaceAcquire(unsafe.Pointer(addr))
 	val = *addr
 	runtime.RaceSemrelease(&mtx)

Load操作は本質的に読み取りですが、ここでもruntime.RaceReadが明示的に呼び出されています。これは、データ競合検出器がアトミックな読み取り操作を適切に追跡し、非アトミックな書き込みとの競合を検出できるようにするためです。

StoreUint32の変更前と変更後:

--- a/src/pkg/sync/atomic/race.go
+++ b/src/pkg/sync/atomic/race.go
@@ -160,6 +178,7 @@ func StoreInt32(addr *int32, val int32) {
 func StoreUint32(addr *uint32, val uint32) {
 	runtime.RaceSemacquire(&mtx)
+	runtime.RaceRead(unsafe.Pointer(addr)) // 追加された行
 	*addr = val
 	runtime.RaceRelease(unsafe.Pointer(addr))
 	runtime.RaceSemrelease(&mtx)

Store操作は書き込みですが、ここでもruntime.RaceReadが呼び出されています。これは、アトミックな書き込み操作が、その書き込みを行う前にメモリ位置の現在の値を「読み取る」という概念的な側面をデータ競合検出器に伝えるためです。これにより、アトミックな書き込みと非アトミックな書き込みの間の競合を検出できます。

これらの変更は、Goのデータ競合検出器が、アトミック操作が関与するより複雑なデータ競合シナリオを正確に識別できるようにするための重要な改善です。これにより、Goの並行プログラミングの堅牢性がさらに向上します。

関連リンク

  • Go言語のデータ競合検出器に関する公式ドキュメントやブログ記事
  • sync/atomicパッケージのドキュメント
  • runtimeパッケージのドキュメント
  • Goの並行処理に関する一般的な情報源

参考にした情報源リンク