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

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

このコミットは、Go言語のランタイムパッケージ内のレース検出器(runtime/race)のテストを安定化(deflake)させることを目的としています。特に、新しいスケジューラが導入されたことにより、テスト中に開始されたゴルーチンが他のテストの実行中に競合状態を報告してしまう問題に対処しています。この変更は、テスト中に開始されたゴルーチンが確実に完了するまで待機するようにすることで、テストの信頼性を向上させています。

コミット

commit 0a9f1ab8bb68835bf66faf9c7a925003c6087c4e
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Fri Feb 8 19:24:50 2013 +0400

    runtime/race: deflake tests
    With the new scheduler races in the tests are reported during execution of other tests.
    The change joins goroutines started during the tests.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/7310066

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

https://github.com/golang/go/commit/0a9f1ab8bb68835bf66faf9c7a925003c6087c4e

元コミット内容

このコミットは、Goランタイムのレース検出器のテストにおいて、新しいスケジューラの導入によって発生していた不安定性(flakiness)を解消することを目的としています。具体的には、テスト内で起動されたゴルーチンが、そのテストが終了した後もバックグラウンドで実行され続け、後続のテストのレース検出に影響を与えてしまう問題がありました。この変更は、テストが終了する前に、そのテスト内で起動されたすべてのゴルーチンが完了するのを待機するように修正することで、この問題を解決しています。

変更の背景

Go言語のランタイムには、データ競合(data race)を検出するためのレース検出器が組み込まれています。これは並行処理のバグを見つける上で非常に強力なツールです。しかし、レース検出器自体のテストは、その性質上、並行処理を多用するため、非常にデリケートです。

このコミットが作成された当時、Goランタイムのスケジューラに大きな変更が加えられました。新しいスケジューラは、ゴルーチンのスケジューリング動作を改善し、より効率的な並行処理を可能にしましたが、その副作用として、レース検出器のテストに不安定性をもたらしました。

具体的には、テストケースAが複数のゴルーチンを起動し、それらのゴルーチンがテストAの終了後も実行され続ける可能性がありました。もしこれらのゴルーチンが、テストBの実行中に共有リソースにアクセスし、テストBのレース検出器がそれを検出した場合、テストBが実際には問題ないにもかかわらず、レース条件を報告してしまうという「偽陽性」が発生していました。これはテストの信頼性を著しく損ない、開発者が実際のバグとテストの不安定性を区別することを困難にしていました。

この問題を解決するため、テスト内で起動されたすべてのゴルーチンが、そのテストが完了する前に確実に終了するように同期メカニズムを導入する必要がありました。これにより、各テストケースが独立した環境で実行され、他のテストケースの実行に影響を与えないようにすることが可能になります。

前提知識の解説

Go言語の並行処理とゴルーチン

Go言語は、軽量なスレッドである「ゴルーチン(goroutine)」と、ゴルーチン間の通信のための「チャネル(channel)」を用いて、並行処理を強力にサポートしています。

  • ゴルーチン: goキーワードを使って関数を呼び出すことで、新しいゴルーチンが起動されます。ゴルーチンはOSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。
  • チャネル: ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは、データの共有ではなく、データの「通信」によって並行処理の同期と調整を行うというGoの哲学("Do not communicate by sharing memory; instead, share memory by communicating.")を体現しています。

Goのレース検出器 (Race Detector)

Goには、プログラム実行中にデータ競合を検出する組み込みのレース検出器があります。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生します。データ競合は、予測不能な動作やクラッシュを引き起こす可能性のある深刻なバグです。 レース検出器は、go run -racego build -racego test -raceなどのコマンドで有効にできます。有効にすると、実行時のパフォーマンスは低下しますが、データ競合を高い精度で検出できます。

sync.WaitGroup

sync.WaitGroupは、複数のゴルーチンの完了を待機するために使用される同期プリミティブです。これは、以下のようなメソッドを提供します。

  • Add(delta int): 待機するゴルーチンの数をdeltaだけ増やします。通常、新しいゴルーチンを起動する直前に呼び出されます。
  • Done(): WaitGroupのカウンタを1減らします。ゴルーチンがそのタスクを完了したときに呼び出されます。
  • Wait(): WaitGroupのカウンタがゼロになるまでブロックします。これにより、すべてのゴルーチンが完了するまでメインゴルーチンが待機できます。

チャネルのバッファリング

Goのチャネルは、バッファリングの有無によって動作が異なります。

  • 非バッファードチャネル: make(chan Type)で作成されます。送信操作は受信操作が行われるまでブロックし、受信操作は送信操作が行われるまでブロックします。これにより、送信側と受信側のゴルーチンが同期されます。
  • バッファードチャネル: make(chan Type, capacity)で作成されます。チャネルにcapacity分の要素を格納できます。バッファが満杯でない限り、送信操作はブロックされません。バッファが空でない限り、受信操作はブロックされません。

技術的詳細

このコミットは、主にsync.WaitGroupとチャネルの適切な使用を通じて、テスト内のゴルーチン同期を強化しています。

TestRaceCrawlにおけるsync.WaitGroupの導入

TestRaceCrawlは、ウェブクローラーのような再帰的なゴルーチン起動パターンを持つテストです。元の実装では、go crawl(uu, d-1)のように新しいゴルーチンを起動していましたが、これらのゴルーチンが完了するのを待つメカニズムがありませんでした。そのため、テスト関数が終了しても、バックグラウンドでcrawlゴルーチンが実行され続け、他のテストに影響を与える可能性がありました。

この問題を解決するために、sync.WaitGroupが導入されました。

  1. var wg sync.WaitGroupWaitGroupのインスタンスが作成されます。
  2. go crawl(url, depth)で最初のcrawlゴルーチンを起動する直前にwg.Add(1)が呼び出され、カウンタがインクリメントされます。
  3. crawl関数内で、go crawl(uu, d-1)で新しい子ゴルーチンを起動するたびにwg.Add(1)が呼び出され、カウンタがインクリメントされます。
  4. crawl関数がその処理を終える直前(特に、子ゴルーチンの起動ループの後)にwg.Done()が呼び出され、カウンタがデクリメントされます。
  5. TestRaceCrawl関数の最後でwg.Wait()が呼び出され、WaitGroupのカウンタがゼロになるまで(つまり、すべてのcrawlゴルーチンが完了するまで)テストの実行がブロックされます。

これにより、TestRaceCrawlが終了する際には、そのテスト内で起動されたすべてのゴルーチンが確実に完了していることが保証され、テストの「deflake」が実現されます。

TestRaceCrawlにおけるチャネルのバッファリング変更

ch := make(chan int)ch := make(chan int, 100)に変更されています。これは、非バッファードチャネルからバッファードチャネルへの変更です。TestRaceCrawlの文脈では、このチャネルはnurlの値を送信するために使用されており、nurlは起動された子ゴルーチンの数をカウントしています。バッファリングを追加することで、nurlの送信が即座にブロックされる可能性が減り、ゴルーチンの起動とチャネルへの送信がよりスムーズに行われるようになります。これは直接的なレース条件の修正というよりは、テストの実行特性を改善し、デッドロックのリスクを減らすための変更であると考えられます。

NewLogにおけるチャネルによる同期

regression_test.goNewLog関数では、LogImpl構造体を初期化し、その直後にゴルーチンを起動してlLogImplのインスタンス)にアクセスしています。元のコードでは、l = LogImpl{}によるlの初期化が、ゴルーチンがlにアクセスするよりも遅れて行われる可能性があり、未初期化のlにアクセスするデータ競合が発生する可能性がありました。

この問題を解決するために、非バッファードチャネルcが導入されました。

  1. c := make(chan bool)でチャネルが作成されます。
  2. ゴルーチン内で_ = llへのアクセス)の後にc <- trueが実行され、チャネルに値が送信されます。
  3. NewLog関数のメインフローでl = LogImpl{}の後に<-cが実行され、チャネルからの値の受信を待ちます。

これにより、NewLog関数がreturnする前に、ゴルーチンがlにアクセスする操作が完了していることが保証されます。これは、テストのセットアップにおける潜在的なデータ競合を解消し、テストの信頼性を高めるための重要な同期メカニズムです。

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

src/pkg/runtime/race/testdata/mop_test.go

--- a/src/pkg/runtime/race/testdata/mop_test.go
+++ b/src/pkg/runtime/race/testdata/mop_test.go
@@ -745,7 +745,8 @@ func TestRaceCrawl(t *testing.T) {
 	url := "dummyurl"
 	depth := 3
 	seen := make(map[string]bool)
-	ch := make(chan int)
+	ch := make(chan int, 100) // チャネルをバッファードに変更
+	var wg sync.WaitGroup     // sync.WaitGroupを導入
 	var crawl func(string, int)
 	crawl = func(u string, d int) {
 		nurl := 0
@@ -759,12 +760,16 @@ func TestRaceCrawl(t *testing.T) {
 		urls := [...]string{"a", "b", "c"}
 		for _, uu := range urls {
 			if _, ok := seen[uu]; !ok {
+				wg.Add(1) // 新しいゴルーチン起動前にカウンタをインクリメント
 				go crawl(uu, d-1)
 				nurl++
 			}
 		}
+		wg.Done() // ゴルーチンがタスクを完了したらカウンタをデクリメント
 	}
+	wg.Add(1) // 最初のゴルーチン起動前にカウンタをインクリメント
 	go crawl(url, depth)
+	wg.Wait() // すべてのゴルーチンが完了するまで待機
 }
 
 func TestRaceIndirection(t *testing.T) {

src/pkg/runtime/race/testdata/regression_test.go

--- a/src/pkg/runtime/race/testdata/regression_test.go
+++ b/src/pkg/runtime/race/testdata/regression_test.go
@@ -15,10 +15,13 @@ type LogImpl struct {
 }
 
 func NewLog() (l LogImpl) {
+	c := make(chan bool) // 同期用チャネルを導入
 	go func() {
 		_ = l
+		c <- true // ゴルーチンがlにアクセスしたらチャネルに送信
 	}()
 	l = LogImpl{}
+	<-c // チャネルからの受信を待ち、ゴルーチンのアクセス完了を同期
 	return
 }
 

コアとなるコードの解説

mop_test.go の変更点

TestRaceCrawl関数は、ウェブクローラーの動作を模倣して、複数のゴルーチンを再帰的に起動します。

  • ch := make(chan int, 100): 元々は非バッファードチャネルでしたが、バッファサイズ100のバッファードチャネルに変更されました。これにより、nurlの送信が即座にブロックされる可能性が減り、ゴルーチンの起動とチャネルへの送信がよりスムーズに行われるようになります。これはテストの実行効率と安定性に寄与します。
  • var wg sync.WaitGroup: sync.WaitGroupが導入され、テスト内で起動されるすべてのcrawlゴルーチンの完了を待機するためのメカニズムが提供されます。
    • wg.Add(1): go crawl(...)で新しいゴルーチンが起動される直前に呼び出されます。これにより、WaitGroupの内部カウンタがインクリメントされ、待機すべきゴルーチンの数が増えます。
    • wg.Done(): crawlゴルーチンがその処理(特に子ゴルーチンの起動)を完了した後に呼び出されます。これにより、カウンタがデクリメントされます。
    • wg.Wait(): TestRaceCrawl関数の最後に呼び出され、WaitGroupのカウンタがゼロになるまで(つまり、すべてのcrawlゴルーチンが完了するまで)テストの実行をブロックします。

これらの変更により、TestRaceCrawlは、そのテスト内で起動されたすべてのゴルーチンが完全に終了するまで待機するようになり、後続のテストに影響を与える可能性のあるバックグラウンドゴルーチンが残らないようにします。これにより、テストの不安定性が解消されます。

regression_test.go の変更点

NewLog関数は、LogImpl構造体を初期化し、そのインスタンスlをゴルーチン内で使用します。

  • c := make(chan bool): 非バッファードチャネルcが導入されました。これは、NewLog関数がreturnする前に、ゴルーチンがlにアクセスする操作が完了したことを確認するための同期ポイントとして機能します。
  • c <- true: ゴルーチン内で_ = llへのアクセス)の直後に実行されます。これにより、ゴルーチンがlにアクセスしたことをNewLog関数のメインフローに通知します。
  • <-c: NewLog関数のメインフローでl = LogImpl{}の直後に実行されます。これにより、チャネルcからの値の受信を待ちます。ゴルーチンがc <- trueを実行するまで、この行でブロックされます。

この同期メカニズムにより、NewLog関数がreturnする時点では、lが完全に初期化され、ゴルーチンがその初期化されたlにアクセスしていることが保証されます。これにより、未初期化のlへのアクセスによる潜在的なデータ競合が解消され、テストの信頼性が向上します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のブログ記事(特にRace Detectorに関するもの)
  • syncパッケージのソースコードとドキュメント
  • Go言語のスケジューラに関する一般的な情報(当時の変更点について)
  • Goのテストにおける並行処理のベストプラクティスに関する一般的な情報

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

このコミットは、Go言語のランタイムパッケージ内のレース検出器(runtime/race)のテストを安定化(deflake)させることを目的としています。特に、Go 1.1で導入された新しいスケジューラが原因で、テスト中に開始されたゴルーチンが他のテストの実行中に競合状態を報告してしまう問題に対処しています。この変更は、テスト中に開始されたゴルーチンが確実に完了するまで待機するようにすることで、テストの信頼性を向上させています。

コミット

commit 0a9f1ab8bb68835bf66faf9c7a925003c6087c4e
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Fri Feb 8 19:24:50 2013 +0400

    runtime/race: deflake tests
    With the new scheduler races in the tests are reported during execution of other tests.
    The change joins goroutines started during the tests.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/7310066

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

https://github.com/golang/go/commit/0a9f1ab8bb68835bf66faf9c7a925003c6087c4e

元コミット内容

このコミットは、Goランタイムのレース検出器のテストにおいて、新しいスケジューラの導入によって発生していた不安定性(flakiness)を解消することを目的としています。具体的には、テスト内で起動されたゴルーチンが、そのテストが終了した後もバックグラウンドで実行され続け、後続のテストのレース検出に影響を与えてしまう問題がありました。この変更は、テストが終了する前に、そのテスト内で起動されたすべてのゴルーチンが完了するのを待機するように修正することで、この問題を解決しています。

変更の背景

Go言語のランタイムには、データ競合(data race)を検出するためのレース検出器が組み込まれています。これは並行処理のバグを見つける上で非常に強力なツールです。しかし、レース検出器自体のテストは、その性質上、並行処理を多用するため、非常にデリケートです。

このコミットが作成された当時、Goランタイムのスケジューラに大きな変更が加えられました。具体的には、2013年5月にリリースされたGo 1.1で、Dmitry Vyukov氏を中心に大幅に改善された新しいスケジューラが導入されました。この新しいスケジューラは、Goの並行処理モデルを強化し、特にマルチコアシステム上でのゴルーチンの管理効率を大幅に向上させました。

Go 1.1のスケジューラの主な特徴は以下の通りです。

  • M:Nスケジューリングモデル: 多数のゴルーチン(G)を少数のOSスレッド(M)に多重化するモデルを採用しています。
  • P (Processor) の導入: スケジューリングのローカルコンテキストとして「Processor」(P)の概念が導入されました。OSスレッド(M)はGoコードを実行するためにPと関連付けられる必要があり、Pの数は通常、CPUコア数に設定されます(GOMAXPROCSで制御可能)。
  • 分散型実行キューとワークスティーリング: Go 1.1以前のスケジューラは単一のグローバル実行キューを使用しており、競合が発生する可能性がありました。新しいスケジューラでは、各Pにローカル実行キューが導入されました。Pのローカルキューが空になった場合、他のPのキューからゴルーチンを「盗む」(ワークスティーリング)ことで、ワークロードの分散とマルチコアの利用率を向上させます。
  • パフォーマンスの向上: これらの変更により、ミューテックスの競合が減少し、スケジューラがより適切な判断(ガベージコレクションのためにスレッドを停止するタイミングなど)を下せるようになったことで、並列Goプログラムのパフォーマンスが劇的に向上しました。

この新しいスケジューラは、Goの進化における重要な一歩であり、その並行処理モデルを強化し、現代のマルチコアアーキテクチャに対してより効率的にしました。

しかし、このスケジューラの変更は、レース検出器のテストに予期せぬ不安定性をもたらしました。具体的には、テストケースAが複数のゴルーチンを起動し、それらのゴルーチンがテストAの終了後も実行され続ける可能性がありました。もしこれらのゴルーチンが、テストBの実行中に共有リソースにアクセスし、テストBのレース検出器がそれを検出した場合、テストBが実際には問題ないにもかかわらず、レース条件を報告してしまうという「偽陽性」が発生していました。これはテストの信頼性を著しく損ない、開発者が実際のバグとテストの不安定性を区別することを困難にしていました。

この問題を解決するため、テスト内で起動されたすべてのゴルーチンが、そのテストが完了する前に確実に終了するように同期メカニズムを導入する必要がありました。これにより、各テストケースが独立した環境で実行され、他のテストケースの実行に影響を与えないようにすることが可能になります。

前提知識の解説

Go言語の並行処理とゴルーチン

Go言語は、軽量なスレッドである「ゴルーチン(goroutine)」と、ゴルーチン間の通信のための「チャネル(channel)」を用いて、並行処理を強力にサポートしています。

  • ゴルーチン: goキーワードを使って関数を呼び出すことで、新しいゴルーチンが起動されます。ゴルーチンはOSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。GoランタイムのスケジューラがこれらのゴルーチンをOSスレッドにマッピングし、効率的に実行します。
  • チャネル: ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは、データの共有ではなく、データの「通信」によって並行処理の同期と調整を行うというGoの哲学("Do not communicate by sharing memory; instead, share memory by communicating.")を体現しています。

Goのレース検出器 (Race Detector)

Goには、プログラム実行中にデータ競合を検出する組み込みのレース検出器があります。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生します。データ競合は、予測不能な動作やクラッシュを引き起こす可能性のある深刻なバグです。 レース検出器は、go run -racego build -racego test -raceなどのコマンドで有効にできます。有効にすると、実行時のパフォーマンスは低下しますが、データ競合を高い精度で検出できます。これは、並行処理のバグを見つけるための非常に強力なデバッグツールです。

sync.WaitGroup

sync.WaitGroupは、複数のゴルーチンの完了を待機するために使用される同期プリミティブです。これは、以下のようなメソッドを提供します。

  • Add(delta int): 待機するゴルーチンの数をdeltaだけ増やします。通常、新しいゴルーチンを起動する直前に呼び出されます。
  • Done(): WaitGroupのカウンタを1減らします。ゴルーチンがそのタスクを完了したときに呼び出されます。
  • Wait(): WaitGroupのカウンタがゼロになるまでブロックします。これにより、すべてのゴルーチンが完了するまでメインゴルーチンが待機できます。 WaitGroupは、特定の処理が完了するまでメインの実行フローを一時停止させたい場合に非常に有用です。

チャネルのバッファリング

Goのチャネルは、バッファリングの有無によって動作が異なります。

  • 非バッファードチャネル: make(chan Type)で作成されます。送信操作は受信操作が行われるまでブロックし、受信操作は送信操作が行われるまでブロックします。これにより、送信側と受信側のゴルーチンが同期され、ハンドシェイクのような動作をします。
  • バッファードチャネル: make(chan Type, capacity)で作成されます。チャネルにcapacity分の要素を格納できます。バッファが満杯でない限り、送信操作はブロックされません。バッファが空でない限り、受信操作はブロックされません。バッファが満杯になると送信はブロックされ、バッファが空になると受信はブロックされます。これにより、送信側と受信側のゴルーチンの結合度が緩やかになります。

技術的詳細

このコミットは、主にsync.WaitGroupとチャネルの適切な使用を通じて、テスト内のゴルーチン同期を強化しています。

TestRaceCrawlにおけるsync.WaitGroupの導入

TestRaceCrawlは、ウェブクローラーのような再帰的なゴルーチン起動パターンを持つテストです。元の実装では、go crawl(uu, d-1)のように新しいゴルーチンを起動していましたが、これらのゴルーチンが完了するのを待つメカニズムがありませんでした。そのため、テスト関数が終了しても、バックグラウンドでcrawlゴルーチンが実行され続け、他のテストに影響を与える可能性がありました。これは、特に新しいスケジューラがゴルーチンの実行順序やタイミングをより動的に決定するようになったことで、顕在化した問題です。

この問題を解決するために、sync.WaitGroupが導入されました。

  1. var wg sync.WaitGroupWaitGroupのインスタンスが作成されます。これは、テスト内で起動されるすべてのcrawlゴルーチンの完了を追跡するためのカウンタとして機能します。
  2. go crawl(url, depth)で最初のcrawlゴルーチンを起動する直前にwg.Add(1)が呼び出され、カウンタがインクリメントされます。
  3. crawl関数内で、go crawl(uu, d-1)で新しい子ゴルーチンを起動するたびにwg.Add(1)が呼び出され、カウンタがインクリメントされます。これにより、新しく起動されたゴルーチンもWaitGroupの監視対象となります。
  4. crawl関数がその処理を終える直前(特に、子ゴルーチンの起動ループの後)にwg.Done()が呼び出され、カウンタがデクリメントされます。これは、そのゴルーチンが完了したことをWaitGroupに通知します。
  5. TestRaceCrawl関数の最後でwg.Wait()が呼び出され、WaitGroupのカウンタがゼロになるまで(つまり、すべてのcrawlゴルーチンが完了するまで)テストの実行がブロックされます。

これにより、TestRaceCrawlが終了する際には、そのテスト内で起動されたすべてのゴルーチンが確実に完了していることが保証され、テストの「deflake」(不安定性の解消)が実現されます。

TestRaceCrawlにおけるチャネルのバッファリング変更

ch := make(chan int)ch := make(chan int, 100)に変更されています。これは、非バッファードチャネルからバッファードチャネルへの変更です。TestRaceCrawlの文脈では、このチャネルはnurlの値を送信するために使用されており、nurlは起動された子ゴルーチンの数をカウントしています。非バッファードチャネルの場合、送信操作は受信操作が行われるまでブロックされるため、ゴルーチンの起動がチャネルの送信によって一時的に停止する可能性がありました。バッファリングを追加することで、nurlの送信が即座にブロックされる可能性が減り、ゴルーチンの起動とチャネルへの送信がよりスムーズに行われるようになります。これは直接的なレース条件の修正というよりは、テストの実行特性を改善し、デッドロックのリスクを減らすための変更であると考えられます。

NewLogにおけるチャネルによる同期

regression_test.goNewLog関数では、LogImpl構造体を初期化し、その直後にゴルーチンを起動してlLogImplのインスタンス)にアクセスしています。元のコードでは、l = LogImpl{}によるlの初期化が、ゴルーチンがlにアクセスするよりも遅れて行われる可能性があり、未初期化のlにアクセスするデータ競合が発生する可能性がありました。これは、特に新しいスケジューラがゴルーチンの実行順序をより柔軟に決定するようになったことで、タイミングの問題として顕在化しやすくなりました。

この問題を解決するために、非バッファードチャネルcが導入されました。

  1. c := make(chan bool)でチャネルが作成されます。これは、NewLog関数がreturnする前に、ゴルーチンがlにアクセスする操作が完了したことを確認するための同期ポイントとして機能します。
  2. ゴルーチン内で_ = llへのアクセス)の後にc <- trueが実行され、チャネルに値が送信されます。これにより、ゴルーチンがlにアクセスしたことをNewLog関数のメインフローに通知します。
  3. NewLog関数のメインフローでl = LogImpl{}の後に<-cが実行され、チャネルからの値の受信を待ちます。ゴルーチンがc <- trueを実行するまで、この行でブロックされます。

この同期メカニズムにより、NewLog関数がreturnする時点では、lが完全に初期化され、ゴルーチンがその初期化されたlにアクセスしていることが保証されます。これにより、未初期化のlへのアクセスによる潜在的なデータ競合が解消され、テストの信頼性が向上します。

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

src/pkg/runtime/race/testdata/mop_test.go

--- a/src/pkg/runtime/race/testdata/mop_test.go
+++ b/src/pkg/runtime/race/testdata/mop_test.go
@@ -745,7 +745,8 @@ func TestRaceCrawl(t *testing.T) {
 	url := "dummyurl"
 	depth := 3
 	seen := make(map[string]bool)
-	ch := make(chan int)
+	ch := make(chan int, 100) // チャネルをバッファードに変更 (容量100)
+	var wg sync.WaitGroup     // sync.WaitGroupを導入
 	var crawl func(string, int)
 	crawl = func(u string, d int) {
 		nurl := 0
@@ -759,12 +760,16 @@ func TestRaceCrawl(t *testing.T) {
 		urls := [...]string{"a", "b", "c"}
 		for _, uu := range urls {
 			if _, ok := seen[uu]; !ok {
+				wg.Add(1) // 新しいゴルーチン起動前にWaitGroupカウンタをインクリメント
 				go crawl(uu, d-1)
 				nurl++
 			}
 		}
+		wg.Done() // ゴルーチンがタスクを完了したらWaitGroupカウンタをデクリメント
 	}
+	wg.Add(1) // 最初のゴルーチン起動前にWaitGroupカウンタをインクリメント
 	go crawl(url, depth)
+	wg.Wait() // すべてのゴルーチンが完了するまで待機
 }
 
 func TestRaceIndirection(t *testing.T) {

src/pkg/runtime/race/testdata/regression_test.go

--- a/src/pkg/runtime/race/testdata/regression_test.go
+++ b/src/pkg/runtime/race/testdata/regression_test.go
@@ -15,10 +15,13 @@ type LogImpl struct {
 }
 
 func NewLog() (l LogImpl) {
+	c := make(chan bool) // 同期用非バッファードチャネルを導入
 	go func() {
 		_ = l
+		c <- true // ゴルーチンがlにアクセスしたらチャネルに送信
 	}()
 	l = LogImpl{}
+	<-c // チャネルからの受信を待ち、ゴルーチンのアクセス完了を同期
 	return
 }
 

コアとなるコードの解説

mop_test.go の変更点

TestRaceCrawl関数は、ウェブクローラーの動作を模倣して、複数のゴルーチンを再帰的に起動します。

  • ch := make(chan int, 100): 元々は非バッファードチャネルでしたが、バッファサイズ100のバッファードチャネルに変更されました。これにより、nurlの送信が即座にブロックされる可能性が減り、ゴルーチンの起動とチャネルへの送信がよりスムーズに行われるようになります。これはテストの実行効率と安定性に寄与します。
  • var wg sync.WaitGroup: sync.WaitGroupが導入され、テスト内で起動されるすべてのcrawlゴルーチンの完了を待機するためのメカニズムが提供されます。
    • wg.Add(1): go crawl(...)で新しいゴルーチンが起動される直前に呼び出されます。これにより、WaitGroupの内部カウンタがインクリメントされ、待機すべきゴルーチンの数が増えます。
    • wg.Done(): crawlゴルーチンがその処理(特に子ゴルーチンの起動)を完了した後に呼び出されます。これにより、カウンタがデクリメントされます。
    • wg.Wait(): TestRaceCrawl関数の最後に呼び出され、WaitGroupのカウンタがゼロになるまで(つまり、すべてのcrawlゴルーチンが完了するまで)テストの実行をブロックします。

これらの変更により、TestRaceCrawlは、そのテスト内で起動されたすべてのゴルーチンが完全に終了するまで待機するようになり、後続のテストに影響を与える可能性のあるバックグラウンドゴルーチンが残らないようにします。これにより、テストの不安定性が解消され、テスト結果の信頼性が向上します。

regression_test.go の変更点

NewLog関数は、LogImpl構造体を初期化し、そのインスタンスlをゴルーチン内で使用します。

  • c := make(chan bool): 非バッファードチャネルcが導入されました。これは、NewLog関数がreturnする前に、ゴルーチンがlにアクセスする操作が完了したことを確認するための同期ポイントとして機能します。
  • c <- true: ゴルーチン内で_ = llへのアクセス)の直後に実行されます。これにより、ゴルーチンがlにアクセスしたことをNewLog関数のメインフローに通知します。
  • <-c: NewLog関数のメインフローでl = LogImpl{}の直後に実行されます。これにより、チャネルcからの値の受信を待ちます。ゴルーチンがc <- trueを実行するまで、この行でブロックされます。

この同期メカニズムにより、NewLog関数がreturnする時点では、lが完全に初期化され、ゴルーチンがその初期化されたlにアクセスしていることが保証されます。これにより、未初期化のlへのアクセスによる潜在的なデータ競合が解消され、テストの信頼性が向上します。これは、Goの並行処理における「通信による共有」の原則を効果的に適用した例と言えます。

関連リンク

参考にした情報源リンク