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

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

このコミットは、Go言語の標準ライブラリであるtestingパッケージにおける重要な改善を導入しています。具体的には、テスト(T型)およびベンチマーク(B型)の実行中に、ログ出力やテストの失敗報告といった操作を並行して行えるようにするための変更です。これにより、並行テストの記述がより安全かつ柔軟になり、特に並行処理を多用するテストケースにおいて、デッドロックやデータ競合のリスクを低減し、テストの信頼性を向上させます。

コミット

commit 9b1412701fc5f265c478153b2032ca90755c38cf
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Jul 25 10:17:27 2012 -0700

    testing: allow concurrent use of T and B

    Notably, allow concurrent logging and failing.

    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/6453045

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

https://github.com/golang/go/commit/9b1412701fc5f265c478153b2032ca90755c38cf

元コミット内容

testing: allow concurrent use of T and B

Notably, allow concurrent logging and failing.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6453045

変更の背景

Go言語のtestingパッケージは、ユニットテストやベンチマークテストを記述するための標準的なフレームワークを提供します。テスト関数やベンチマーク関数は、それぞれ*testing.T*testing.Bのインスタンスを受け取り、これらを通じてログ出力(Log, Logf)、エラー報告(Error, Errorf, Fail, FailNow)、ベンチマーク操作(N, StopTimer, StartTimerなど)を行います。

このコミット以前は、testing.Ttesting.Bの内部状態(特にテストの出力バッファoutputや失敗フラグfailed)は、複数のゴルーチンから同時にアクセスされた場合にデータ競合(data race)を引き起こす可能性がありました。例えば、t.Log()t.Fail()といったメソッドが異なるゴルーチンから同時に呼び出された場合、内部の共有データが保護されていないため、予期せぬ動作やクラッシュが発生する可能性がありました。

Goのテストは、しばしば並行処理をテストするために、テスト関数内で複数のゴルーチンを起動します。このようなシナリオでは、各ゴルーチンが独立して*testing.Tのメソッドを呼び出すことが想定されます。しかし、共有リソースへの非同期アクセスが適切に同期されていないと、テスト自体が不安定になり、テスト結果の信頼性が損なわれるという問題がありました。

この変更の背景には、testingパッケージの堅牢性を高め、並行テストの記述をより安全かつ容易にするという目的があります。特に、並行ログ出力や並行失敗報告を可能にすることで、開発者は並行処理のテストにおいて、*testing.T*testing.Bのメソッド呼び出しに関する同期の心配をすることなく、テストロジックに集中できるようになります。

前提知識の解説

Go言語のtestingパッケージ

Go言語の標準ライブラリに含まれるtestingパッケージは、Goプログラムの自動テストをサポートします。

  • *testing.T: ユニットテスト関数(func TestXxx(t *testing.T))に渡される型です。テストのログ出力、エラー報告、テストのスキップなどの機能を提供します。
  • *testing.B: ベンチマークテスト関数(func BenchmarkXxx(b *testing.B))に渡される型です。コードのパフォーマンスを測定するための機能を提供します。
  • Fail(): テストを失敗としてマークしますが、テスト関数の実行は継続します。
  • Failed(): テストが失敗としてマークされているかどうかを返します。
  • Log() / Logf(): テストの実行中にメッセージを出力します。これらのメッセージは、テストが失敗した場合や、-vフラグが指定された場合に表示されます。

Go言語の並行処理と同期プリミティブ

Go言語は、ゴルーチン(goroutine)とチャネル(channel)という強力なプリミティブを用いて並行処理をサポートします。しかし、複数のゴルーチンが共有メモリにアクセスする際には、データ競合を防ぐための同期メカニズムが必要です。

  • データ競合 (Data Race): 複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生します。データ競合は予測不能な結果やプログラムのクラッシュを引き起こす可能性があります。
  • syncパッケージ: Goの標準ライブラリsyncパッケージは、並行処理における同期プリミティブを提供します。
    • sync.Mutex: 排他ロック(mutual exclusion lock)を提供します。Lock()メソッドでロックを取得し、Unlock()メソッドでロックを解放します。一度に1つのゴルーチンのみがロックを保持できます。主に書き込み操作の保護に使用されます。
    • sync.RWMutex: 読み書きロック(reader-writer mutex)を提供します。
      • RLock() / RUnlock(): 読み込みロックを取得/解放します。複数のゴルーチンが同時に読み込みロックを保持できます。
      • Lock() / Unlock(): 書き込みロックを取得/解放します。書き込みロックが保持されている間は、他の読み込みロックも書き込みロックも取得できません。 RWMutexは、読み込み操作が頻繁で書き込み操作が少ない場合に、Mutexよりも高い並行性を提供できます。

技術的詳細

このコミットの核心は、testingパッケージの内部構造であるcommon型にsync.RWMutexを導入し、共有される状態(outputfailed)へのアクセスを同期することです。

common型は、*testing.T*testing.Bの両方に共通する基盤となる構造体であり、テストの出力バッファ(output []byte)とテストの失敗状態(failed bool)を保持しています。これらのフィールドは、Fail(), Failed(), log()などのメソッドによって読み書きされます。

変更前は、これらのフィールドへのアクセスは同期されていませんでした。そのため、複数のゴルーチンが同時にこれらのメソッドを呼び出すと、データ競合が発生する可能性がありました。

このコミットでは、以下の変更が行われました。

  1. sync.RWMutexの追加: common構造体にmu sync.RWMutexフィールドが追加されました。このミューテックスは、outputfailedフィールドへのアクセスを保護するために使用されます。

    type common struct {
        mu     sync.RWMutex // guards output and failed
        output []byte       // Output generated by test or benchmark.
        failed bool         // Test or benchmark has failed.
        // ...
    }
    
  2. Fail()メソッドの変更: Fail()メソッドは、c.failed = trueという書き込み操作を行うため、mu.Lock()mu.Unlock()で保護されるようになりました。これにより、複数のゴルーチンが同時にFail()を呼び出しても、failedフィールドへの書き込みが排他的に行われ、データ競合が防止されます。

    func (c *common) Fail() {
        c.mu.Lock()
        defer c.mu.Unlock()
        c.failed = true
    }
    
  3. Failed()メソッドの変更: Failed()メソッドは、c.failedという読み込み操作を行うため、mu.RLock()mu.RUnlock()で保護されるようになりました。RWMutexの特性により、複数のゴルーチンが同時にFailed()を呼び出すことができ、読み込み操作の並行性が維持されます。

    func (c *common) Failed() bool {
        c.mu.RLock()
        defer c.mu.RUnlock()
        return c.failed
    }
    
  4. log()メソッドの変更: log()メソッドは、c.outputにデータを追加する書き込み操作を行うため、mu.Lock()mu.Unlock()で保護されるようになりました。これにより、並行してログ出力が行われても、outputバッファへの書き込みが安全に行われます。

    func (c *common) log(s string) {
        c.mu.Lock()
        defer c.mu.Unlock()
        c.output = append(c.output, decorate(s)...)
    }
    
  5. report()およびRunTests()でのFailed()の使用: t.failedへの直接アクセスが、同期されたt.Failed()メソッドの呼び出しに置き換えられました。これにより、テスト結果の報告時にもfailed状態への安全な読み込みが保証されます。

これらの変更により、*testing.T*testing.Bのインスタンスが複数のゴルーチン間で共有され、同時にメソッドが呼び出された場合でも、内部状態の整合性が保たれ、データ競合が回避されるようになりました。特に、ログ出力やテストの失敗報告といった操作が並行して行えるようになったことで、並行処理をテストする際の柔軟性と信頼性が大幅に向上しました。

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

src/pkg/testing/testing.go ファイルが変更されています。

--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -87,6 +87,7 @@ import (
 	"runtime/pprof"
 	"strconv"
 	"strings"
+	"sync" // 追加
 	"time"
 )

@@ -116,8 +117,10 @@ var (
 // common holds the elements common between T and B and
 // captures common methods such as Errorf.
 type common struct {
-	output   []byte    // Output generated by test or benchmark.
-	failed   bool      // Test or benchmark has failed.
+	mu     sync.RWMutex // guards output and failed // 追加
+	output []byte       // Output generated by test or benchmark.
+	failed bool         // Test or benchmark has failed.
+
 	start    time.Time // Time test or benchmark started
 	duration time.Duration
 	self     interface{}      // To be sent on signal channel when done.
@@ -176,10 +179,18 @@ type T struct {
 }

 // Fail marks the function as having failed but continues execution.
-func (c *common) Fail() { c.failed = true }
+func (c *common) Fail() {
+	c.mu.Lock() // 追加
+	defer c.mu.Unlock() // 追加
+	c.failed = true
+}

 // Failed returns whether the function has failed.
-func (c *common) Failed() bool { return c.failed }
+func (c *common) Failed() bool {
+	c.mu.RLock() // 追加
+	defer c.mu.RUnlock() // 追加
+	return c.failed
+}

 // FailNow marks the function as having failed and stops its execution.
 // Execution will continue at the next test or benchmark.
@@ -210,6 +221,8 @@ func (c *common) FailNow() {

 // log generates the output. It's always at the same stack depth.
 func (c *common) log(s string) {
+	c.mu.Lock() // 追加
+	defer c.mu.Unlock() // 追加
 	c.output = append(c.output, decorate(s)...)
 }

@@ -303,7 +316,7 @@ func Main(matchString func(pat, str string) (bool, error), tests []InternalTest,
 func (t *T) report() {
 	tstr := fmt.Sprintf("(%.2f seconds)", t.duration.Seconds())
 	format := "--- %s: %s %s\n%s"
-	if t.failed { // 変更
+	if t.Failed() { // 変更
 		fmt.Printf(format, "FAIL", t.name, tstr, t.output)
 	} else if *chatty {
 		fmt.Printf(format, "PASS", t.name, tstr, t.output)
@@ -362,7 +375,7 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
 				continue
 			}
 			t.report()
-			ok = ok && !out.failed // 変更
+			ok = ok && !out.Failed() // 変更
 		}

 		running := 0
@@ -375,7 +388,7 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
 			}
 			t := (<-collector).(*T)
 			t.report()
-			ok = ok && !t.failed // 変更
+			ok = ok && !t.Failed() // 変更
 			running--
 		}
 	}

コアとなるコードの解説

このコミットの主要な変更点は、testingパッケージのcommon構造体にsync.RWMutex(読み書きミューテックス)を追加し、このミューテックスを使って共有される状態(outputfailed)へのアクセスを同期したことです。

  1. import "sync" の追加: syncパッケージがインポートされ、sync.RWMutexを使用できるようになりました。

  2. common構造体へのmu sync.RWMutexの追加: common構造体は、*testing.T*testing.Bが内部的に共有する基盤となる構造体です。ここにmu sync.RWMutexが追加され、output(テスト出力バッファ)とfailed(テスト失敗フラグ)という2つのフィールドを保護する役割を担います。

  3. Fail()メソッドの変更:

    • 変更前: func (c *common) Fail() { c.failed = true }
    • 変更後: func (c *common) Fail() { c.mu.Lock(); defer c.mu.Unlock(); c.failed = true } Fail()c.failedというブール値をtrueに設定する書き込み操作です。複数のゴルーチンが同時にFail()を呼び出すと、c.failedへのデータ競合が発生する可能性があります。c.mu.Lock()defer c.mu.Unlock()を追加することで、この操作が排他的に行われるようになり、failedフィールドの整合性が保証されます。
  4. Failed()メソッドの変更:

    • 変更前: func (c *common) Failed() bool { return c.failed }
    • 変更後: func (c *common) Failed() bool { c.mu.RLock(); defer c.mu.RUnlock(); return c.failed } Failed()c.failedの値を読み込む操作です。読み込み操作は、書き込み操作と競合しない限り、複数のゴルーチンが同時に行っても安全です。c.mu.RLock()defer c.mu.RUnlock()を使用することで、複数のゴルーチンが同時にFailed()を呼び出してfailedの状態を読み取ることが可能になり、読み込みの並行性が向上します。これはsync.RWMutexの大きな利点です。
  5. log()メソッドの変更:

    • 変更前: func (c *common) log(s string) { c.output = append(c.output, decorate(s)...) }
    • 変更後: func (c *common) log(s string) { c.mu.Lock(); defer c.mu.Unlock(); c.output = append(c.output, decorate(s)...) } log()c.outputというバイトスライスにデータを追加する書き込み操作です。スライスへのappendは、基盤となる配列の再割り当てや要素のコピーを伴う可能性があり、複数のゴルーチンから同時に行われるとデータ競合を引き起こします。c.mu.Lock()defer c.mu.Unlock()で保護することで、ログ出力が排他的に行われ、outputバッファの整合性が保たれます。
  6. report()およびRunTests()でのt.Failed()の利用: 以前はt.failedフィールドに直接アクセスしていましたが、この変更により、t.Failed()メソッドを介してアクセスするようになりました。これにより、failed状態の読み込みもsync.RWMutexによって適切に同期されるようになり、テスト結果の報告時にも安全性が確保されます。

これらの変更により、*testing.T*testing.Bのインスタンスが複数のゴルーチン間で共有され、同時にメソッドが呼び出された場合でも、内部状態の整合性が保たれ、データ競合が回避されるようになりました。特に、ログ出力やテストの失敗報告といった操作が並行して行えるようになったことで、並行処理をテストする際の柔軟性と信頼性が大幅に向上しました。

関連リンク

参考にした情報源リンク

  • Go言語のsyncパッケージドキュメント: https://pkg.go.dev/sync
  • Go言語のtestingパッケージドキュメント: https://pkg.go.dev/testing
  • Go言語における並行処理の概念(ゴルーチン、チャネル、ミューテックスなど)に関する一般的な情報源(例: Go言語の公式ドキュメント、Effective Goなど)