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

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

このコミットは、Go言語の標準ライブラリ io/ioutil パッケージにおけるデータ競合(data race)の問題を修正するものです。具体的には、io.Discard(以前は ioutil.Discard)の実装において、devNull 型の ReadFrom メソッドが内部的に使用するバッファ blackHole が、レース検出器(race detector)によってデータ競合として報告される問題を解決しています。この修正は、ビルドタグ(build tags)を活用し、レース検出器が有効なビルドとそうでないビルドで異なる実装を使い分けることで実現されています。

コミット

commit 373dbcb37af7b8966fc6f3818701c9ca3e8693da
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Sun Oct 7 22:08:06 2012 +0400

    io/ioutil: fix data race under the race detector
    See issue 3970 (it's already marked as Fixed).
    
    R=rsc, minux.ma
    CC=golang-dev
    https://golang.org/cl/6624059

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

https://github.com/golang/go/commit/373dbcb37af7b8966fc6f3818701c9ca3e8693da

元コミット内容

io/ioutil パッケージにおいて、レース検出器が有効な環境でデータ競合が発生する問題を修正しました。関連するIssue 3970は既に修正済みとしてマークされています。

変更の背景

このコミットの背景には、Go言語の並行処理における重要な概念である「データ競合」と、それを検出するための「レース検出器」の存在があります。

io/ioutil パッケージには、データを読み捨てるための Discard(Go 1.16以降は io.Discard に移動)というインターフェースがあります。この Discard の内部実装である devNull 型の Write メソッドや ReadFrom メソッドは、読み込んだデータを一時的に保持するための内部バッファ blackHole を使用していました。

問題は、この blackHole バッファがグローバル変数として定義されており、複数のゴルーチンが同時に Discard を使用して ReadFrom メソッドを呼び出した場合、同じ blackHole バッファに対して同時に読み書きが発生する可能性があったことです。Goのレース検出器は、このような並行アクセスをデータ競合として検出し、警告を発します。

データ競合は、プログラムの予測不能な動作やバグの原因となるため、Goでは非常に重視されています。レース検出器は開発者がこれらの問題を早期に発見し、修正するのに役立つツールです。このコミットは、レース検出器によって報告された具体的なデータ競合(Issue 3970)を解決するために行われました。

前提知識の解説

データ競合 (Data Race)

データ競合とは、複数のゴルーチン(またはスレッド)が同時に同じメモリ位置にアクセスし、そのうち少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生する状態です。データ競合が発生すると、プログラムの実行結果が非決定論的になり、予測不能なバグ(クラッシュ、不正なデータ、セキュリティ脆弱性など)につながる可能性があります。

Go言語のメモリモデルでは、データ競合は未定義の動作を引き起こすとされています。これは、データ競合が発生した場合、コンパイラやランタイムがどのような動作をするか保証されないことを意味します。

Goのレース検出器 (Go Race Detector)

Goには、プログラム実行中にデータ競合を検出するための組み込みツールである「レース検出器」があります。これは、Go 1.1以降で利用可能になりました。レース検出器を有効にしてプログラムを実行すると、共有メモリへのアクセスを監視し、データ競合のパターンを検出した場合に警告を出力します。

レース検出器を有効にするには、Goコマンドに -race フラグを付けてビルドまたは実行します。 例: go run -race main.go または go build -race -o myapp

レース検出器は、プログラムの実行速度を低下させる(通常2〜20倍遅くなる)ため、本番環境での実行には適していませんが、開発中やテスト中にデータ競合を発見するための非常に強力なツールです。

ビルドタグ (Build Tags)

Goのビルドタグ(またはビルド制約)は、特定の条件に基づいてソースファイルをコンパイルに含めるか除外するかを制御するためのメカニズムです。これは、ファイルの上部に // +build tag_name の形式でコメントとして記述されます。

  • // +build tag_name: このファイルは tag_name が指定された場合にのみコンパイルされます。
  • // +build !tag_name: このファイルは tag_name が指定されていない場合にのみコンパイルされます。

このコミットでは、race というビルドタグが使用されています。

  • blackhole.go には // +build !race が付与されており、レース検出器が有効でない(-race フラグがない)場合にコンパイルされます。
  • blackhole_race.go には // +build race が付与されており、レース検出器が有効な(-race フラグがある)場合にコンパイルされます。

これにより、レース検出器の有無に応じて、異なる実装の blackHole 関数が選択的にコンパイルされ、データ競合の問題を回避しつつ、通常ビルドではパフォーマンスを維持することができます。

技術的詳細

このコミットの技術的な解決策は、Goのビルドタグを巧みに利用して、レース検出器の有無によって blackHole バッファの挙動を切り替える点にあります。

元々の実装では、src/pkg/io/ioutil/ioutil.go 内に var blackHole = make([]byte, 8192) というグローバル変数が定義されており、これが devNull.ReadFrom メソッドで共有されていました。

修正後のアプローチは以下の通りです。

  1. blackhole.go の導入 (!race ビルド用): このファイルには // +build !race というビルドタグが付与されています。

    var blackHoleBuf = make([]byte, 8192)
    
    func blackHole() []byte {
        return blackHoleBuf
    }
    

    この実装では、blackHoleBuf というグローバルなスライスを定義し、blackHole() 関数が常にこの同じスライスを返します。これは、レース検出器が有効でない通常のビルドパスで使用され、バッファの再割り当てを避けることでパフォーマンスを維持します。データ競合は発生しないと仮定されます(または、発生しても検出されないため問題にならないと判断されます)。

  2. blackhole_race.go の導入 (race ビルド用): このファイルには // +build race というビルドタグが付与されています。

    // Replaces the normal fast implementation with slower but formally correct one.
    func blackHole() []byte {
        return make([]byte, 8192)
    }
    

    この実装では、blackHole() 関数が呼び出されるたびに、新しい []byte スライスを make で作成して返します。これにより、各 ReadFrom 呼び出しが独自のバッファを持つことになり、複数のゴルーチンが同時に ReadFrom を呼び出しても、共有されたメモリにアクセスすることがなくなり、データ競合が解消されます。このアプローチはバッファの再割り当てが頻繁に発生するためパフォーマンスは低下しますが、レース検出器が有効な環境では正確な競合検出が優先されます。

  3. ioutil.go の変更: 元々 ioutil.go にあったグローバル変数 blackHole は削除され、devNull.ReadFrom メソッド内で blackHole() 関数を呼び出すように変更されました。

    // 変更前
    // var blackHole = make([]byte, 8192)
    // readSize, err = r.Read(blackHole)
    
    // 変更後
    buf := blackHole()
    readSize, err = r.Read(buf)
    

    これにより、コンパイル時に選択された blackHole() 関数の実装(グローバルバッファを返すか、新しいバッファを返すか)が使用されるようになります。

この修正により、レース検出器が有効なビルドではデータ競合が報告されなくなり、通常のビルドでは既存のパフォーマンス特性が維持されるという、両方の要件を満たす解決策が実現されました。

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

--- a/src/pkg/io/ioutil/blackhole.go
+++ b/src/pkg/io/ioutil/blackhole.go
@@ -0,0 +1,13 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build !race
+
+package ioutil
+
+var blackHoleBuf = make([]byte, 8192)
+
+func blackHole() []byte {
+	return blackHoleBuf
+}
diff --git a/src/pkg/io/ioutil/blackhole_race.go b/src/pkg/io/ioutil/blackhole_race.go
new file mode 100644
index 0000000000..eb640e05cf
--- /dev/null
+++ b/src/pkg/io/ioutil/blackhole_race.go
@@ -0,0 +1,13 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build race
+
+package ioutil
+
+// Replaces the normal fast implementation with slower but formally correct one.
+
+func blackHole() []byte {
+	return make([]byte, 8192)
+}
diff --git a/src/pkg/io/ioutil/ioutil.go b/src/pkg/io/ioutil/ioutil.go
index f072b8c754..31c77299ee 100644
--- a/src/pkg/io/ioutil/ioutil.go
+++ b/src/pkg/io/ioutil/ioutil.go
@@ -130,12 +130,11 @@ func (devNull) Write(p []byte) (int, error) {
 	return len(p), nil
 }
 
-var blackHole = make([]byte, 8192)
-
 func (devNull) ReadFrom(r io.Reader) (n int64, err error) {\n+\tbuf := blackHole()\n \treadSize := 0
 \tfor {\n-\t\treadSize, err = r.Read(blackHole)\n+\t\treadSize, err = r.Read(buf)\n \t\tn += int64(readSize)\n \t\tif err != nil {\n \t\t\tif err == io.EOF {\n```

## コアとなるコードの解説

上記の差分は、以下の主要な変更を示しています。

1.  **`src/pkg/io/ioutil/blackhole.go` の新規追加**:
    *   このファイルは `// +build !race` ビルドタグを持ち、レース検出器が有効でない場合にのみコンパイルされます。
    *   `blackHoleBuf` という8192バイトのグローバルなバイトスライスを定義しています。
    *   `blackHole()` 関数は、この `blackHoleBuf` を返します。これにより、通常ビルドでは単一の共有バッファが再利用され、パフォーマンスが最適化されます。

2.  **`src/pkg/io/ioutil/blackhole_race.go` の新規追加**:
    *   このファイルは `// +build race` ビルドタグを持ち、レース検出器が有効な場合にのみコンパイルされます。
    *   `blackHole()` 関数は、呼び出されるたびに新しい8192バイトのバイトスライスを `make` で作成して返します。
    *   これにより、レース検出器が有効なビルドでは、各 `ReadFrom` 呼び出しが独立したバッファを使用するため、共有メモリへの同時アクセスによるデータ競合が回避されます。コメントにもあるように、これは「通常の高速な実装を、より遅いが形式的に正しいものに置き換える」ものです。

3.  **`src/pkg/io/ioutil/ioutil.go` の変更**:
    *   既存のグローバル変数 `var blackHole = make([]byte, 8192)` が削除されました。これは、新しい `blackhole.go` または `blackhole_race.go` で定義される `blackHole()` 関数にその役割が移管されたためです。
    *   `devNull` 型の `ReadFrom` メソッド内で、`r.Read()` に渡すバッファが、直接グローバル変数 `blackHole` を参照するのではなく、`blackHole()` 関数を呼び出して取得するように変更されました。
        *   変更前: `readSize, err = r.Read(blackHole)`
        *   変更後: `buf := blackHole()` `readSize, err = r.Read(buf)`
    *   この変更により、ビルドタグによって選択された `blackHole()` 関数の実装が動的に適用され、レース検出器の有無に応じた適切なバッファ管理が行われるようになります。

この一連の変更により、Goのビルドシステムとレース検出器の機能を最大限に活用し、パフォーマンスと並行処理の安全性の両立が図られています。

## 関連リンク

*   Go CL (Code Review): [https://golang.org/cl/6624059](https://golang.org/cl/6624059)
*   Go Issue 3970: [https://golang.org/issue/3970](https://golang.org/issue/3970) (このコミットで修正された問題)

## 参考にした情報源リンク

*   Go Race Detector: [https://go.dev/doc/articles/race_detector](https://go.dev/doc/articles/race_detector)
*   Go Build Constraints (Build Tags): [https://go.dev/cmd/go/#hdr-Build_constraints](https://go.dev/cmd/go/#hdr-Build_constraints)
*   Go `io.Discard` (Go 1.16以降): [https://pkg.go.dev/io#Discard](https://pkg.go.dev/io#Discard)
*   Go `io/ioutil` (Go 1.16で非推奨): [https://pkg.go.dev/io/ioutil](https://pkg.go.dev/io/ioutil)
*   Go Memory Model: [https://go.dev/ref/mem](https://go.dev/ref/mem)