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

[インデックス 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.Readerstrings.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.Readerstrings.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.Readerstrings.ReaderReadAtメソッドが、io.ReaderAtインターフェースの並行安全性に関する契約(すなわち、内部状態を変更しないこと)を遵守していることを、Goのレース検出器を用いて検証する点にあります。

具体的には、TestReaderAtConcurrentという新しいテスト関数がbytes/reader_test.gostrings/reader_test.goの両方に追加されています。このテストは以下の手順で動作します。

  1. bytes.NewReaderまたはstrings.NewReaderを使用して、テスト対象のReaderインスタンスを作成します。初期データとして、例えば"0123456789"のような文字列が使用されます。
  2. sync.WaitGroupが初期化されます。これは、複数のゴルーチンの完了を待機するために使用されます。
  3. ループ内で複数のゴルーチン(このコミットでは5つのゴルーチン)が起動されます。
  4. 各ゴルーチンは、異なるオフセット(iの値)でReader.ReadAtメソッドを呼び出します。
  5. ReadAt呼び出し後、各ゴルーチンはwg.Done()を呼び出して完了を通知します。
  6. メインのテストゴルーチンはwg.Wait()を呼び出し、すべての並行ゴルーチンが完了するのを待ちます。

このテストの目的は、ReadAtメソッドが並行して呼び出されたときに、Readerインスタンスの内部状態(例えば、bytes.Readerstrings.Readerが持つiフィールドやprevRuneフィールドなど)に対して書き込み操作を行わないことを確認することです。もしReadAtがこれらの内部状態を誤って変更しようとすると、複数のゴルーチンからの同時アクセスによってデータ競合が発生し、go test -raceでテストを実行した際にレース検出器がそれを報告します。

このテストは、ReadAtが「状態を変更しない」というio.ReaderAtの重要な特性を強制するための防御的なプログラミングの一環として機能します。

また、bytes/reader.gostrings/reader.goReadAtメソッドの冒頭に、// cannot modify state - see io.ReaderAtというコメントが追加されています。これは、このメソッドが内部状態を変更してはならないという設計上の制約を明示的に示すものであり、将来のコード変更者がこの原則を理解し、遵守するのに役立ちます。

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

このコミットでは、以下の4つのファイルが変更されています。

  1. src/pkg/bytes/reader.go
  2. src/pkg/bytes/reader_test.go
  3. src/pkg/strings/reader.go
  4. 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.gosrc/pkg/strings/reader.goReadAtメソッドに追加された// cannot modify state - see io.ReaderAtというコメントは、このメソッドの設計上の重要な制約を強調しています。これは、io.ReaderAtインターフェースの契約の一部であり、ReadAtが並行して安全に呼び出されるためには、レシーバの内部状態を変更してはならないことを意味します。このコメントは、コードの可読性を高め、将来のメンテナンス担当者がこの制約を誤って破ることを防ぐためのものです。

TestReaderAtConcurrentテスト関数

このテスト関数は、Goのレース検出器の機能を最大限に活用して、ReadAtメソッドの並行安全性を検証します。

  1. r := NewReader([]byte("0123456789")) (または strings.NewReader): テスト対象となるReaderインスタンスを初期化します。このインスタンスは、複数のゴルーチン間で共有されます。

  2. var wg sync.WaitGroup: sync.WaitGroupは、複数のゴルーチンが完了するのを待つための同期プリミティブです。これにより、すべての並行ReadAt呼び出しが完了するまでテストが終了しないことが保証されます。

  3. for i := 0; i < 5; i++ { ... go func(i int) { ... } (i) }: このループは5つのゴルーチンを起動します。各ゴルーチンは、異なるオフセットir.ReadAt(buf[:], int64(i))を呼び出します。

    • wg.Add(1): 各ゴルーチンを起動する前に、WaitGroupのカウンタをインクリメントします。
    • defer wg.Done(): 各ゴルーチンが終了する際に、WaitGroupのカウンタをデクリメントします。これにより、ゴルーチンが正常に完了したか、パニックで終了したかにかかわらず、カウンタが確実にデクリメントされます。
    • var buf [1]byte: 読み取り結果を格納するための一時的なバッファです。ReadAtはデータをこのバッファに書き込みます。
    • r.ReadAt(buf[:], int64(i)): これがテストの核心部分です。複数のゴルーチンが同時に同じReaderインスタンスのReadAtメソッドを呼び出します。
  4. wg.Wait(): メインのテストゴルーチンは、すべての並行ゴルーチンがwg.Done()を呼び出すまでここでブロックします。

このテストの設計は、ReadAtが内部状態を変更しないという前提に基づいています。もしReadAtが内部状態(例えば、Reader構造体内のフィールド)を読み取り以外の目的でアクセス(書き込み)しようとすると、複数のゴルーチンからの同時書き込みアクセスによってデータ競合が発生します。go test -raceでこのテストを実行すると、レース検出器がこの競合を検出し、エラーとして報告します。これにより、ReadAtの実装がio.ReaderAtの並行安全性要件を満たしていることが保証されます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goのソースコード
  • GoのGerritコードレビューシステム