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

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

このコミットは、Go言語のnetパッケージにおいて、Windows環境でのネットワークI/O操作(ReadおよびWrite)にGoのレース検出器(Race Detector)のためのアノテーションを追加するものです。これにより、ネットワーク操作におけるデータ競合の検出精度が向上します。具体的には、fd_windows.go内のReadおよびWriteメソッドにraceAcquireraceReleaseMergeの呼び出しが追加され、レース検出器がこれらのI/O操作を同期イベントとして認識できるようになります。また、レース検出器が有効な場合と無効な場合で異なる実装を提供するrace.gorace0.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 -racego build -racego test -raceなどのコマンドで有効にできます。レース検出器は、プログラムの実行時にメモリアクセスを監視し、同期されていない共有メモリへのアクセスを特定します。

データ競合 (Data Race)

データ競合は、以下の3つの条件がすべて満たされたときに発生します。

  1. 少なくとも2つのゴルーチンが同じメモリ位置にアクセスする。
  2. 少なくとも1つのアクセスが書き込みである。
  3. それらのアクセスが同期メカニズム(ミューテックス、チャネルなど)によって保護されていない。

データ競合は、プログラムの動作を非決定的にし、デバッグが困難なバグを引き起こす可能性があります。

Happens-Before 関係

並行プログラミングにおいて、「happens-before」関係は、イベント間の順序付けを定義する概念です。あるイベントAがイベントBの前に「happens-before」する場合、Aの結果はBから観測可能であることが保証されます。レース検出器は、このhappens-before関係を利用して、同期されていないメモリアクセスを特定します。同期プリミティブ(ミューテックスのロック/アンロック、チャネルの送受信など)は、このhappens-before関係を確立します。

runtime.RaceAcquireruntime.RaceReleaseMerge

これらはGoランタイムの内部関数であり、レース検出器に対して明示的に同期イベントを通知するために使用されます。

  • runtime.RaceAcquire(addr unsafe.Pointer): この関数は「acquire」操作を確立します。これは、指定されたアドレスaddrに対する以前のRaceReleaseまたはRaceReleaseMerge操作の後に発生したすべてのメモリ書き込みが、このRaceAcquireを実行するゴルーチンから見えるようになることをレース検出器に通知します。C11メモリモデルのatomic_load with memory_order_acquireに似ています。
  • runtime.RaceRelease(addr unsafe.Pointer): この関数は「release」操作を実行します。これは、このRaceRelease呼び出しの前に現在のゴルーチンによってaddrに対して行われたすべてのメモリ書き込みが、同じaddrに対して後続のRaceAcquireを実行する他のゴルーチンから見えるようになることをレース検出器に通知します。C11メモリモデルのatomic_store with memory_order_releaseに似ています。
  • runtime.RaceReleaseMerge(addr unsafe.Pointer): RaceReleaseに似ていますが、happens-before関係の「マージ」を意味します。これは、複数のリリース操作が後続のacquire操作のために論理的に結合される必要があるシナリオで使用されます。RaceAcquireは、addrに対する以前のRaceReleaseMerge、およびaddrに対する最後のRaceReleaseを含む、happens-before関係を確立します。

これらの関数は、Goの標準的な同期プリミティブでは検出できないような、低レベルのカスタム同期ポイントについてレース検出器に手動で通知するメカニズムを提供します。

技術的詳細

このコミットの主要な目的は、Windows環境におけるnetパッケージのI/O操作(ReadWrite)が、レース検出器によって適切に同期イベントとして扱われるようにすることです。

Goのレース検出器は、通常、ミューテックスやチャネルなどの高レベルの同期プリミティブを監視することでデータ競合を検出します。しかし、OSの非同期I/Oメカニズム(WindowsのCompletion Portsなど)を使用する低レベルのネットワーク操作では、これらの高レベルのプリミティブが直接関与しない場合があります。その結果、I/Oバッファへのアクセスが複数のゴルーチン間で競合しても、レース検出器がそれを検出できない可能性があります。

この問題を解決するために、コミットではruntime.RaceAcquireruntime.RaceReleaseMergeというランタイム関数が利用されています。これらの関数は、レース検出器に対して、特定のメモリ位置(この場合はioSyncというグローバル変数)に対するアクセスが同期イベントであることを明示的に伝えます。

具体的には、以下のロジックが導入されています。

  1. ioSync変数の導入: src/pkg/net/fd_windows.goioSync uint64というグローバル変数が追加されました。この変数は、実際のデータではなく、レース検出器がI/O操作の同期を追跡するための「同期オブジェクト」として機能します。
  2. Read操作でのraceAcquire: fd_windows.goReadメソッド内で、読み込みが完了した後にraceAcquire(unsafe.Pointer(&ioSync))が呼び出されます。これは、Read操作が完了した時点で、その操作によって読み込まれたデータが他のゴルーチンから安全にアクセス可能になることをレース検出器に通知します。つまり、Read操作は、以前のWrite操作によって「リリース」されたデータを「取得」する操作として扱われます。
  3. Write操作でのraceReleaseMerge: fd_windows.goWriteメソッド内で、書き込みが完了する直前にraceReleaseMerge(unsafe.Pointer(&ioSync))が呼び出されます。これは、Write操作によってバッファに書き込まれたデータが、他のゴルーチンがRead操作を通じて「取得」できるようになることをレース検出器に通知します。raceReleaseMergeが使用されるのは、複数の書き込み操作が論理的に結合され、後続の読み込み操作によって取得される可能性があるためです。
  4. レース検出器の有効/無効に応じた実装:
    • src/pkg/net/race.goは、ビルドタグ+build raceが指定されている場合にコンパイルされます。このファイルでは、raceenabledtrueに設定され、raceAcquireraceReleaseMergeraceReadRangeraceWriteRangeがそれぞれruntimeパッケージの対応する関数を呼び出すように実装されています。
    • src/pkg/net/race0.goは、ビルドタグ+build !raceが指定されている場合にコンパイルされます。このファイルでは、raceenabledfalseに設定され、すべてのrace*関数が空の関数として実装されています。これにより、レース検出器が無効なビルドでは、これらのアノテーションによるオーバーヘッドがなくなります。

この変更により、Goのレース検出器は、ネットワークI/O操作におけるバッファへのアクセスを、明示的な同期イベントとして認識できるようになります。これにより、I/Oバッファの再利用や並行アクセスに関連するデータ競合が、より確実に検出されるようになります。

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

このコミットでは、以下の3つのファイルが変更または新規作成されています。

  1. src/pkg/net/fd_windows.go:

    • ioSyncというuint64型のグローバル変数が追加されました。
    • Readメソッドの戻り値の処理部分に、raceenabledtrueの場合にraceAcquire(unsafe.Pointer(&ioSync))を呼び出すコードが追加されました。
    • Writeメソッドのdefer fd.writeUnlock()の直後に、raceenabledtrueの場合にraceReleaseMerge(unsafe.Pointer(&ioSync))を呼び出すコードが追加されました。
  2. src/pkg/net/race.go (新規作成):

    • +build raceおよび+build windowsビルドタグを持つ新規ファイル。
    • raceenabled定数をtrueに設定。
    • runtime.RaceAcquireruntime.RaceReleaseMergeruntime.RaceReadRangeruntime.RaceWriteRangeをラップするraceAcquireraceReleaseMergeraceReadRangeraceWriteRange関数を定義。
  3. src/pkg/net/race0.go (新規作成):

    • +build !raceおよび+build windowsビルドタグを持つ新規ファイル。
    • raceenabled定数をfalseに設定。
    • すべてのrace*関数(raceAcquireraceReleaseMergeraceReadRangeraceWriteRange)を空の関数として定義。

コアとなるコードの解説

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変数は、ReadWrite操作間の同期ポイントとしてレース検出器に認識させるためのダミーのアドレスとして機能します。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のようにレース検出器が有効なビルド時にコンパイルされます。raceenabledtrueに設定され、raceAcquireraceReleaseMergeといった関数が、実際に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) {
	// 何もしない
}

このファイルは、レース検出器が無効なビルド時にコンパイルされます。raceenabledfalseに設定され、すべてのrace*関数が空の関数として実装されています。これにより、レース検出器が不要な場合には、これらのアノテーションによるパフォーマンスオーバーヘッドが完全に排除されます。これは、Goのビルドタグシステムを活用した典型的な条件付きコンパイルの例です。

関連リンク

参考にした情報源リンク