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

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

このコミットは、Go言語の標準ライブラリ io/ioutil パッケージにおける Discard 関数(およびその内部で使用される devNull 型)に存在していたデータ競合(data race)を修正するものです。具体的には、Discard が内部的に使用するバッファの再利用メカニズムに起因する競合状態を解消し、並行処理環境下での安全性を確保します。

コミット

io/ioutil: Discard のデータ競合を修正

Issue #4589 を修正

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

https://github.com/golang/go/commit/eb43ce2d7711ad963de4860b70495a7aba3271c5

元コミット内容

commit eb43ce2d7711ad963de4860b70495a7aba3271c5
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Fri Dec 28 09:33:22 2012 -0800

    io/ioutil: fix Discard data race
    
    Fixes #4589
    
    R=golang-dev, iant, dvyukov
    CC=golang-dev
    https://golang.org/cl/7011047

変更の背景

このコミットは、Go言語の標準ライブラリ io/ioutil パッケージの Discard 関数に存在していたデータ競合のバグを修正するために行われました。Discard 関数は、io.Reader からデータを読み込みますが、そのデータをどこにも書き込まずに破棄する役割を持ちます。これは、ストリームからデータを効率的に読み飛ばしたい場合などに使用されます。

問題は、Discard が内部的に devNull 型の ReadFrom メソッドを呼び出し、このメソッドがデータを読み込むための一時的なバッファとして、グローバルに定義された単一の []byte スライス blackHoleBuf を再利用していた点にありました。複数のゴルーチンが同時に Discard を呼び出すと、これらのゴルーチンが同じ blackHoleBuf スライスに同時にアクセスし、読み書きを行う可能性がありました。これにより、バッファの内容が予期せず変更されたり、スライスヘッダが破損したりする「データ競合」が発生し、プログラムのクラッシュや不正な動作を引き起こす可能性がありました。

この問題は、Goのレース検出器(race detector)によって検出され、Issue #4589 として報告されました。レース検出器は、並行プログラムにおけるデータ競合を特定するためのツールであり、このバグの発見に貢献しました。

前提知識の解説

io.Readerio.Writer

Go言語における io.Readerio.Writer は、それぞれデータの読み込みと書き込みのためのインターフェースです。

  • io.Reader インターフェースは Read(p []byte) (n int, err error) メソッドを定義し、データをバイトスライス p に読み込み、読み込んだバイト数 n とエラー err を返します。
  • io.Writer インターフェースは Write(p []byte) (n int, err error) メソッドを定義し、バイトスライス p のデータを書き込み、書き込んだバイト数 n とエラー err を返します。

io/ioutil パッケージと ioutil.Discard

io/ioutil パッケージは、I/O操作を補助するユーティリティ関数を提供します。 ioutil.Discard は、io.Writer インターフェースを実装した特殊なオブジェクトです。このオブジェクトに書き込まれたデータはすべて破棄されます。io.Copy(ioutil.Discard, reader) のように使用することで、reader からのデータを効率的に読み捨てることができます。

データ競合 (Data Race)

データ競合は、複数のゴルーチン(またはスレッド)が同時に同じメモリ位置にアクセスし、そのうち少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生する並行処理のバグです。データ競合が発生すると、プログラムの動作が非決定論的になり、予期せぬ結果やクラッシュを引き起こす可能性があります。

Go言語には、go run -race コマンドで有効にできる組み込みのレース検出器があり、実行時にデータ競合を検出するのに役立ちます。

Goのチャネル (Channels)

Goのチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、Goの並行処理モデルの中心的な要素であり、共有メモリによるデータ競合を避けるための安全な手段を提供します。チャネルは、バッファリングされたものとバッファリングされていないものがあります。バッファリングされたチャネルは、指定された数の要素を保持でき、バッファが満杯になるまで送信はブロックされません。

select ステートメント

select ステートメントは、複数のチャネル操作を待機するために使用されます。select は、準備ができたチャネル操作のいずれかを実行します。default ケースを含めることで、どのチャネル操作も準備ができていない場合にすぐに実行される非ブロッキングな select を作成できます。

技術的詳細

変更前の問題点

変更前は、io/ioutil/blackhole.go に以下のようなコードがありました。

// +build !race

package ioutil

var blackHoleBuf = make([]byte, 8192)

func blackHole() []byte {
	return blackHoleBuf
}

そして、io/ioutil/ioutil.godevNull 型の ReadFrom メソッドでこの blackHoleBuf を使用していました。

func (devNull) ReadFrom(r io.Reader) (n int64, err error) {
	buf := blackHole() // ここでグローバルな blackHoleBuf を取得
	readSize := 0
	for {
		readSize, err = r.Read(buf) // 複数のゴルーチンが同時に buf に書き込む可能性
		n += int64(readSize)
		if err != nil {
			break
		}
	}
	return n, err
}

blackHoleBuf はグローバル変数であり、blackHole() 関数は常にこの同じ []byte スライスを返していました。したがって、複数のゴルーチンが同時に devNull.ReadFrom を呼び出すと、すべてが同じ blackHoleBuf を共有し、r.Read(buf) の呼び出しによって同時にその内容を書き換えようとします。これがデータ競合の原因でした。

また、blackhole_race.go というファイルが存在し、+build race タグが付いていました。これは、レース検出器が有効なビルドの場合に、blackHole() 関数が毎回新しいバッファを生成するようにすることで、データ競合を回避する(ただしパフォーマンスは低下する)という暫定的な対策でした。しかし、これは根本的な解決策ではなく、レース検出器が有効でない通常のビルドではデータ競合が依然として発生しました。

変更後の解決策

このコミットでは、blackHoleBuf を単一の []byte スライスから、[]byte スライスを格納できるバッファリングされたチャネル(容量1)に変更することで、データ競合を解決しています。

var blackHoleBuf = make(chan []byte, 1)

これにより、バッファの再利用がチャネルを介して同期的に行われるようになります。

  • blackHole() 関数:

    • select ステートメントと default ケースを使用して、blackHoleBuf チャネルからバッファを非ブロッキングで取得しようとします。
    • チャネルにバッファがあればそれを使用し、なければ新しい 8192 バイトのバッファを作成して返します。
    • これにより、常に有効なバッファが提供され、かつチャネルからの取得は同期的に行われるため、複数のゴルーチンが同時に同じバッファにアクセスするのを防ぎます。
  • blackHolePut() 関数:

    • select ステートメントと default ケースを使用して、使用済みのバッファを blackHoleBuf チャネルに非ブロッキングで戻そうとします。
    • チャネルに空きがあればバッファを戻し、なければ(チャネルがすでに満杯であれば)バッファは単にガベージコレクションの対象となります。
    • これにより、バッファの再利用が可能になり、メモリ割り当てのオーバーヘッドを削減できます。
  • devNull.ReadFrom() メソッド:

    • blackHole() でバッファを取得した後、defer blackHolePut(buf) を追加しています。
    • これにより、ReadFrom メソッドが終了する際に、取得したバッファが必ず blackHoleBuf チャネルに戻されるようになります。

この変更により、blackHoleBuf は単一のバッファを安全に再利用するための「プール」として機能し、データ競合を回避しながらパフォーマンスを維持できるようになりました。また、blackhole_race.go ファイルは不要になったため削除されました。

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

--- a/src/pkg/io/ioutil/blackhole.go
+++ b/src/pkg/io/ioutil/blackhole.go
@@ -2,12 +2,22 @@
 // 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)
+var blackHoleBuf = make(chan []byte, 1)
 
 func blackHole() []byte {
-	return blackHoleBuf
+	select {
+	case b := <-blackHoleBuf:
+		return b
+	default:
+	}
+	return make([]byte, 8192)
+}
+
+func blackHolePut(p []byte) {
+	select {
+	case blackHoleBuf <- p:
+	default:
+	}
 }
diff --git a/src/pkg/io/ioutil/blackhole_race.go b/src/pkg/io/ioutil/blackhole_race.go
deleted file mode 100644
index eb640e05cf..0000000000
--- a/src/pkg/io/ioutil/blackhole_race.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// 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 31c77299ee..0eb146c0ab 100644
--- a/src/pkg/io/ioutil/ioutil.go
+++ b/src/pkg/io/ioutil/ioutil.go
@@ -132,6 +132,7 @@ func (devNull) Write(p []byte) (int, error) {\n 
 func (devNull) ReadFrom(r io.Reader) (n int64, err error) {\n 	buf := blackHole()\n+\tdefer blackHolePut(buf)\n 	readSize := 0\n 	for {\n 	\treadSize, err = r.Read(buf)\n```

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

### `src/pkg/io/ioutil/blackhole.go` の変更

1.  **`blackHoleBuf` の型変更**:
    -   変更前: `var blackHoleBuf = make([]byte, 8192)` (グローバルな `[]byte` スライス)
    -   変更後: `var blackHoleBuf = make(chan []byte, 1)` (容量1の `[]byte` チャネル)
    -   これにより、`blackHoleBuf` は単一のバッファを安全に受け渡しするための同期プリミティブとして機能します。

2.  **`blackHole()` 関数の変更**:
    -   変更前は単にグローバルな `blackHoleBuf` を返していました。
    -   変更後:
        ```go
        func blackHole() []byte {
        	select {
        	case b := <-blackHoleBuf: // チャネルからバッファを取得しようと試みる
        		return b
        	default: // チャネルが空の場合
        	}
        	return make([]byte, 8192) // 新しいバッファを作成して返す
        }
        ```
        この関数は、まず `blackHoleBuf` チャネルから既存のバッファを取得しようとします。`select` と `default` を組み合わせることで、チャネルが空の場合でもブロックせずにすぐに新しいバッファを割り当てて返します。これにより、バッファの再利用を試みつつ、常にバッファが利用可能であることを保証します。

3.  **`blackHolePut()` 関数の追加**:
    -   新しい関数 `blackHolePut(p []byte)` が追加されました。
    -   ```go
        func blackHolePut(p []byte) {
        	select {
        	case blackHoleBuf <- p: // バッファをチャネルに戻そうと試みる
        	default: // チャネルが満杯の場合
        	}
        }
        ```
        この関数は、使用済みのバッファ `p` を `blackHoleBuf` チャネルに戻そうとします。`select` と `default` を使用することで、チャネルがすでに満杯の場合でもブロックせずに、バッファを単に破棄します(ガベージコレクションの対象となる)。これにより、バッファの再利用メカニズムが安全かつ非ブロッキングで機能します。

### `src/pkg/io/ioutil/blackhole_race.go` の削除

-   このファイルは、レース検出器が有効な場合にのみコンパイルされ、データ競合を回避するために毎回新しいバッファを割り当てるという暫定的な対策を提供していました。
-   `blackhole.go` の変更によってデータ競合が根本的に解決されたため、このファイルは不要となり削除されました。

### `src/pkg/io/ioutil/ioutil.go` の変更

1.  **`devNull.ReadFrom()` メソッドへの `defer` の追加**:
    -   `buf := blackHole()` の直後に `defer blackHolePut(buf)` が追加されました。
    -   `defer` ステートメントは、囲む関数(この場合は `ReadFrom`)がリターンする直前に、指定された関数呼び出し(`blackHolePut(buf)`)を実行することを保証します。
    -   これにより、`ReadFrom` メソッドが正常に終了するか、エラーで終了するかにかかわらず、`blackHole()` で取得したバッファが必ず `blackHolePut()` を通じて `blackHoleBuf` チャネルに戻されるようになります。これは、リソース(この場合はバッファ)の適切な解放と再利用を保証するための重要なパターンです。

これらの変更により、`ioutil.Discard` が内部的に使用するバッファの取得と解放がチャネルを介して同期的に行われるようになり、複数のゴルーチンからの同時アクセスによるデータ競合が安全に回避されるようになりました。

## 関連リンク

*   Go Issue #4589: [https://github.com/golang/go/issues/4589](https://github.com/golang/go/issues/4589)
*   Go CL 7011047: [https://golang.org/cl/7011047](https://golang.org/cl/7011047) (このコミットに対応するGoのコードレビューシステム上の変更リスト)

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

*   Go言語公式ドキュメント:
    *   `io` パッケージ: [https://pkg.go.dev/io](https://pkg.go.dev/io)
    *   `io/ioutil` パッケージ: [https://pkg.go.dev/io/ioutil](https://pkg.go.dev/io/ioutil) (Go 1.16以降は非推奨となり、機能は`io`と`os`パッケージに移行されていますが、このコミット時点では現役でした)
    *   Go言語の並行処理 (Concurrency in Go): [https://go.dev/doc/effective_go#concurrency](https://go.dev/doc/effective_go#concurrency)
    *   Go Race Detector: [https://go.dev/doc/articles/race_detector](https://go.dev/doc/articles/race_detector)
*   Go言語におけるチャネルの利用: [https://gobyexample.com/channels](https://gobyexample.com/channels)
*   Go言語における `select` ステートメント: [https://gobyexample.com/select](https://gobyexample.com/select)
*   Go言語における `defer` ステートメント: [https://gobyexample.com/defer](https://gobyexample.com/defer)
*   Go言語におけるデータ競合の解説記事 (例: The Go Blog - Data Races): [https://go.dev/blog/race-detector](https://go.dev/blog/race-detector)
*   Go言語におけるバッファプールの実装パターン (例: `sync.Pool`): [https://pkg.go.dev/sync#Pool](https://pkg.go.dev/sync#Pool) (このコミットではチャネルが使われているが、バッファ再利用の一般的な概念として)