[インデックス 19101] ファイルの概要
このコミットは、Go言語のbytes
パッケージとstrings
パッケージにおけるReader.ReadAt
メソッドの並行処理安全性(特にデータ競合の有無)を検証するためのテストを追加するものです。ReadAt
メソッドが内部状態を変更しないことを保証し、Goのレース検出器(Race Detector)が潜在的な競合を捕捉できるようにするためのテストケースが導入されています。
コミット
commit 2dbc5d26c773e4400c0adfc25d9160eeaf6530b0
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Apr 10 15:46:07 2014 -0700
bytes, strings: add Reader.ReadAt race tests
Tests for the race detector to catch anybody
trying to mutate Reader in ReadAt.
LGTM=gri
R=gri
CC=golang-codereviews
https://golang.org/cl/86700043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2dbc5d26c773e4400c0adfc25d9160eeaf6530b0
元コミット内容
bytes, strings: add Reader.ReadAt race tests
このコミットは、bytes
パッケージとstrings
パッケージのReader.ReadAt
メソッドに対して、レース検出器がデータ競合を検出するためのテストを追加します。これは、ReadAt
メソッドがReader
の内部状態を変更しようとする試みを捕捉することを目的としています。
変更の背景
io.ReaderAt
インターフェースの仕様では、ReadAt
メソッドは呼び出し元のオフセットに基づいてデータを読み取るべきであり、その操作は並行して安全であるべきです。つまり、ReadAt
メソッドは、そのレシーバ(この場合はbytes.Reader
やstrings.Reader
のインスタンス)の内部状態を読み取り専用でアクセスし、変更してはなりません。もしReadAt
が内部状態を変更してしまうと、複数のゴルーチンから同時に呼び出された場合にデータ競合が発生し、予測不能な動作やクラッシュにつながる可能性があります。
このコミットは、このような潜在的なデータ競合を防ぐために、Goのレース検出器を活用したテストを追加することで、Reader.ReadAt
の実装がio.ReaderAt
の並行安全性に関する契約を遵守していることを保証しようとしています。これにより、将来的に誰かが誤ってReadAt
メソッド内でReader
の内部状態をミューテートするような変更を加えても、テストがそれを検出し、問題が本番環境にデプロイされる前に修正されるようになります。
前提知識の解説
1. Goのレース検出器 (Race Detector)
Goには、プログラム実行中に発生するデータ競合(data race)を検出するための組み込みツールである「レース検出器」があります。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生します。データ競合は、プログラムの動作を予測不能にし、デバッグが困難なバグの一般的な原因となります。
レース検出器は、プログラムの実行を監視し、データ競合のパターンを特定します。検出された場合、競合が発生した場所、アクセスタイプ(読み取り/書き込み)、および関連するゴルーチンのスタックトレースを含む詳細なレポートを出力します。Goのテスト実行時にgo test -race
フラグを使用することで有効にできます。
2. io.ReaderAt
インターフェース
io.ReaderAt
はGoの標準ライブラリio
パッケージで定義されているインターフェースです。
type ReaderAt interface {
ReadAt(p []byte, off int64) (n int, err error)
}
このインターフェースは、指定されたオフセットoff
からp
にデータを読み込むReadAt
メソッドを定義します。重要なのは、ReadAt
のドキュメントに「ReadAt
は、呼び出し元のオフセットを変更せず、複数のゴルーチンから同時に呼び出されても安全である」と明記されている点です。これは、ReadAt
の実装が、レシーバの内部状態(例えば、現在の読み取り位置を示すオフセットなど)を変更してはならないことを意味します。これにより、ファイルやネットワーク接続などの共有リソースから、複数の並行する読み取り操作を安全に行うことができます。
3. bytes.Reader
と strings.Reader
bytes.Reader
:[]byte
スライスをラップし、io.Reader
,io.ReaderAt
,io.Seeker
,io.ByteReader
インターフェースを実装する型です。バイトスライスをあたかもファイルのように読み取ることができます。strings.Reader
:string
をラップし、io.Reader
,io.ReaderAt
,io.Seeker
,io.ByteReader
インターフェースを実装する型です。文字列をあたかもファイルのように読み取ることができます。
これらのReader
型は、内部に読み取り位置を示すオフセットを持っていますが、ReadAt
メソッドはこのオフセットを直接使用せず、引数として渡されたoff
に基づいて読み取りを行います。したがって、ReadAt
はこれらのReader
の内部状態を変更すべきではありません。
技術的詳細
このコミットの技術的な核心は、bytes.Reader
とstrings.Reader
のReadAt
メソッドが、io.ReaderAt
インターフェースの並行安全性に関する契約(すなわち、内部状態を変更しないこと)を遵守していることを、Goのレース検出器を用いて検証する点にあります。
具体的には、TestReaderAtConcurrent
という新しいテスト関数がbytes/reader_test.go
とstrings/reader_test.go
の両方に追加されています。このテストは以下の手順で動作します。
bytes.NewReader
またはstrings.NewReader
を使用して、テスト対象のReader
インスタンスを作成します。初期データとして、例えば"0123456789"
のような文字列が使用されます。sync.WaitGroup
が初期化されます。これは、複数のゴルーチンの完了を待機するために使用されます。- ループ内で複数のゴルーチン(このコミットでは5つのゴルーチン)が起動されます。
- 各ゴルーチンは、異なるオフセット(
i
の値)でReader.ReadAt
メソッドを呼び出します。 ReadAt
呼び出し後、各ゴルーチンはwg.Done()
を呼び出して完了を通知します。- メインのテストゴルーチンは
wg.Wait()
を呼び出し、すべての並行ゴルーチンが完了するのを待ちます。
このテストの目的は、ReadAt
メソッドが並行して呼び出されたときに、Reader
インスタンスの内部状態(例えば、bytes.Reader
やstrings.Reader
が持つi
フィールドやprevRune
フィールドなど)に対して書き込み操作を行わないことを確認することです。もしReadAt
がこれらの内部状態を誤って変更しようとすると、複数のゴルーチンからの同時アクセスによってデータ競合が発生し、go test -race
でテストを実行した際にレース検出器がそれを報告します。
このテストは、ReadAt
が「状態を変更しない」というio.ReaderAt
の重要な特性を強制するための防御的なプログラミングの一環として機能します。
また、bytes/reader.go
とstrings/reader.go
のReadAt
メソッドの冒頭に、// cannot modify state - see io.ReaderAt
というコメントが追加されています。これは、このメソッドが内部状態を変更してはならないという設計上の制約を明示的に示すものであり、将来のコード変更者がこの原則を理解し、遵守するのに役立ちます。
コアとなるコードの変更箇所
このコミットでは、以下の4つのファイルが変更されています。
src/pkg/bytes/reader.go
src/pkg/bytes/reader_test.go
src/pkg/strings/reader.go
src/pkg/strings/reader_test.go
src/pkg/bytes/reader.go
および src/pkg/strings/reader.go
ReadAt
メソッドの定義にコメントが追加されています。
--- a/src/pkg/bytes/reader.go
+++ b/src/pkg/bytes/reader.go
@@ -43,6 +43,7 @@ func (r *Reader) Read(b []byte) (n int, err error) {
}
func (r *Reader) ReadAt(b []byte, off int64) (n int, err error) {
+ // cannot modify state - see io.ReaderAt
if off < 0 {
return 0, errors.New("bytes: invalid offset")
}
strings/reader.go
も同様です。
src/pkg/bytes/reader_test.go
および src/pkg/strings/reader_test.go
新しいテスト関数TestReaderAtConcurrent
が追加されています。
--- a/src/pkg/bytes/reader_test.go
+++ b/src/pkg/bytes/reader_test.go
@@ -10,6 +10,7 @@ import (
"io"
"io/ioutil"
"os"
+ "sync"
"testing"
)
@@ -98,6 +99,22 @@ func TestReaderAt(t *testing.T) {
}
}
+func TestReaderAtConcurrent(t *testing.T) {
+ // Test for the race detector, to verify ReadAt doesn't mutate
+ // any state.
+ r := NewReader([]byte("0123456789"))
+ var wg sync.WaitGroup
+ for i := 0; i < 5; i++ {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ var buf [1]byte
+ r.ReadAt(buf[:], int64(i))
+ }(i)
+ }
+ wg.Wait()
+}
+
func TestReaderWriteTo(t *testing.T) {
for i := 0; i < 30; i += 3 {
var l int
strings/reader_test.go
も同様で、strings.NewReader
を使用している点が異なります。
コアとなるコードの解説
ReadAt
メソッドへのコメント追加
src/pkg/bytes/reader.go
とsrc/pkg/strings/reader.go
のReadAt
メソッドに追加された// cannot modify state - see io.ReaderAt
というコメントは、このメソッドの設計上の重要な制約を強調しています。これは、io.ReaderAt
インターフェースの契約の一部であり、ReadAt
が並行して安全に呼び出されるためには、レシーバの内部状態を変更してはならないことを意味します。このコメントは、コードの可読性を高め、将来のメンテナンス担当者がこの制約を誤って破ることを防ぐためのものです。
TestReaderAtConcurrent
テスト関数
このテスト関数は、Goのレース検出器の機能を最大限に活用して、ReadAt
メソッドの並行安全性を検証します。
-
r := NewReader([]byte("0123456789"))
(またはstrings.NewReader
): テスト対象となるReader
インスタンスを初期化します。このインスタンスは、複数のゴルーチン間で共有されます。 -
var wg sync.WaitGroup
:sync.WaitGroup
は、複数のゴルーチンが完了するのを待つための同期プリミティブです。これにより、すべての並行ReadAt
呼び出しが完了するまでテストが終了しないことが保証されます。 -
for i := 0; i < 5; i++ { ... go func(i int) { ... } (i) }
: このループは5つのゴルーチンを起動します。各ゴルーチンは、異なるオフセットi
でr.ReadAt(buf[:], int64(i))
を呼び出します。wg.Add(1)
: 各ゴルーチンを起動する前に、WaitGroup
のカウンタをインクリメントします。defer wg.Done()
: 各ゴルーチンが終了する際に、WaitGroup
のカウンタをデクリメントします。これにより、ゴルーチンが正常に完了したか、パニックで終了したかにかかわらず、カウンタが確実にデクリメントされます。var buf [1]byte
: 読み取り結果を格納するための一時的なバッファです。ReadAt
はデータをこのバッファに書き込みます。r.ReadAt(buf[:], int64(i))
: これがテストの核心部分です。複数のゴルーチンが同時に同じReader
インスタンスのReadAt
メソッドを呼び出します。
-
wg.Wait()
: メインのテストゴルーチンは、すべての並行ゴルーチンがwg.Done()
を呼び出すまでここでブロックします。
このテストの設計は、ReadAt
が内部状態を変更しないという前提に基づいています。もしReadAt
が内部状態(例えば、Reader
構造体内のフィールド)を読み取り以外の目的でアクセス(書き込み)しようとすると、複数のゴルーチンからの同時書き込みアクセスによってデータ競合が発生します。go test -race
でこのテストを実行すると、レース検出器がこの競合を検出し、エラーとして報告します。これにより、ReadAt
の実装がio.ReaderAt
の並行安全性要件を満たしていることが保証されます。
関連リンク
- Go Race Detector: https://go.dev/doc/articles/race_detector
io.ReaderAt
documentation: https://pkg.go.dev/io#ReaderAtbytes.Reader
documentation: https://pkg.go.dev/bytes#Readerstrings.Reader
documentation: https://pkg.go.dev/strings#Reader- Go CL 86700043 (Gerrit review): https://golang.org/cl/86700043
参考にした情報源リンク
- Go言語の公式ドキュメント
- Goのソースコード
- GoのGerritコードレビューシステム