[インデックス 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
メソッドで共有されていました。
修正後のアプローチは以下の通りです。
-
blackhole.go
の導入 (!race
ビルド用): このファイルには// +build !race
というビルドタグが付与されています。var blackHoleBuf = make([]byte, 8192) func blackHole() []byte { return blackHoleBuf }
この実装では、
blackHoleBuf
というグローバルなスライスを定義し、blackHole()
関数が常にこの同じスライスを返します。これは、レース検出器が有効でない通常のビルドパスで使用され、バッファの再割り当てを避けることでパフォーマンスを維持します。データ競合は発生しないと仮定されます(または、発生しても検出されないため問題にならないと判断されます)。 -
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
を呼び出しても、共有されたメモリにアクセスすることがなくなり、データ競合が解消されます。このアプローチはバッファの再割り当てが頻繁に発生するためパフォーマンスは低下しますが、レース検出器が有効な環境では正確な競合検出が優先されます。 -
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)