[インデックス 19048] ファイルの概要
src/pkg/net/tcp_test.go
は、Go言語の標準ライブラリである net
パッケージにおけるTCP通信機能のテストおよびベンチマークを定義するファイルです。このファイルには、TCP接続の確立、データの送受信、およびタイムアウト処理などのネットワーク操作のパフォーマンスを測定するためのベンチマーク関数が含まれています。特に、benchmarkTCP
関数は、指定された並行数で多数のTCP接続を確立し、データのやり取りを行うことで、TCPスタックの性能を評価します。
コミット
commit 8076f21e8ea9cd3fc7d0fd23b2262fce662e4bde
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Apr 7 11:00:07 2014 +0400
net: fix data race in benchmark
If an error happens on a connection, server goroutine can call b.Logf
after benchmark finishes.
So join both client and server goroutines.
Update #7718
LGTM=bradfitz
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/84750047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8076f21e8ea9cd3fc7d0fd23b2262fce662e4bde
元コミット内容
このコミットは、Goの net
パッケージ内のベンチマークテストにおいて発生していたデータ競合を修正するものです。具体的には、TCP接続でエラーが発生した場合に、サーバー側のゴルーチンがベンチマークの実行が終了した後でも b.Logf
メソッドを呼び出す可能性があり、これが問題を引き起こしていました。この問題を解決するため、クライアントとサーバーの両方のゴルーチンがベンチマークのメイン処理と適切に同期されるように変更が加えられました。この修正は、Issue #7718に関連しています。
変更の背景
この変更の主な背景は、Goのベンチマーク実行中に発生する潜在的なデータ競合の解消です。src/pkg/net/tcp_test.go
内の benchmarkTCP
関数は、多数の並行TCP接続を処理するサーバーとクライアントのゴルーチンを起動します。
問題は、サーバー側のゴルーチンが接続処理中にエラーに遭遇した場合、そのエラーを b.Logf
を使ってログに記録しようとすることにありました。しかし、ベンチマークのメイン関数 (benchmarkTCP
) がすでに終了し、testing.B
オブジェクトが解放またはクリーンアップされた後でも、まだ実行中のサーバーゴルーチンが b.Logf
を呼び出そうとする可能性がありました。
testing.B
オブジェクトはベンチマークの内部状態(タイミング、イテレーション数、ロギングバッファなど)を管理しており、ベンチマークが終了するとその状態は確定されます。ベンチマーク終了後に b.Logf
が呼び出されると、解放されたメモリや不正な状態の testing.B
オブジェクトにアクセスしようとすることになり、これがデータ競合を引き起こし、予測不能な動作やベンチマークの不安定化を招く可能性がありました。
このコミットは、クライアントとサーバーの両方のゴルーチンが、ベンチマークのメイン関数がリターンする前に確実に終了するように同期メカニズムを導入することで、このデータ競合を解消することを目的としています。これにより、b.Logf
が testing.B
オブジェクトの有効なライフサイクル内でのみ呼び出されることが保証されます。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の概念とテストフレームワークに関する知識が役立ちます。
-
Goの
net
パッケージとTCP通信:net
パッケージは、ネットワークI/Oプリミティブを提供するGoの標準ライブラリです。TCP/IP、UDP、Unixドメインソケットなどのネットワークプログラミングをサポートします。net.Listen
: 指定されたネットワークアドレスで着信接続をリッスンするためのリスナーを作成します。TCPサーバーの開始点となります。net.Dial
: 指定されたネットワークアドレスへの接続を確立します。TCPクライアントの開始点となります。net.Conn
: ネットワーク接続を表すインターフェースで、データの読み書き (Read
,Write
) や接続のクローズ (Close
) などのメソッドを提供します。
-
Goの
testing
パッケージとベンチマーク (testing.B
):- Goには、ユニットテスト、ベンチマークテスト、例示テストをサポートする組み込みの
testing
パッケージがあります。 - ベンチマークテスト: コードのパフォーマンスを測定するために使用されます。関数名のプレフィックスが
Benchmark
で始まる関数として定義されます (例:func BenchmarkMyFunction(b *testing.B)
)。 testing.B
: ベンチマーク関数に渡される構造体で、ベンチマークの実行を制御し、結果を報告するためのメソッドを提供します。b.N
: ベンチマーク対象の操作を繰り返す回数。testing
パッケージが自動的に調整し、統計的に有意な結果が得られるようにします。b.ResetTimer()
: タイマーをリセットし、セットアップコードの時間を測定から除外します。b.Logf(...)
: ベンチマーク中に情報をログに出力するためのメソッドです。これはtesting.B
オブジェクトの内部状態に書き込むため、ベンチマークのライフサイクル外で呼び出されるとデータ競合の原因となる可能性があります。b.Fatalf(...)
: 致命的なエラーを報告し、ベンチマークを停止します。
- Goには、ユニットテスト、ベンチマークテスト、例示テストをサポートする組み込みの
-
Goのゴルーチン (Goroutine) とチャネル (Channel):
- ゴルーチン: Goの並行処理の基本単位です。非常に軽量なスレッドのようなもので、
go
キーワードを使って関数呼び出しの前に置くことで簡単に起動できます (例:go myFunction()
)。 - チャネル: ゴルーチン間で安全にデータを送受信するための通信メカニズムです。チャネルは、Goの「メモリを共有して通信するのではなく、通信によってメモリを共有する」という並行処理の哲学を体現しています。
make(chan Type)
: バッファなしチャネルを作成します。送信操作は受信操作が準備できるまでブロックし、受信操作は送信操作が準備できるまでブロックします。これにより、厳密な同期ポイントが作成されます。make(chan Type, capacity)
: バッファありチャネルを作成します。capacity
で指定された数の要素をバッファに格納できます。バッファが満杯でない限り送信はブロックせず、バッファが空でない限り受信はブロックしません。セマフォやワークキューの実装によく使われます。ch <- value
: チャネルch
にvalue
を送信します。value := <-ch
: チャネルch
から値を受信します。
- ゴルーチン: Goの並行処理の基本単位です。非常に軽量なスレッドのようなもので、
-
データ競合 (Data Race):
- 複数のゴルーチンが同時に同じメモリ領域にアクセスし、そのうち少なくとも1つのアクセスが書き込み操作である場合に、適切な同期なしで発生する競合状態です。
- データ競合は、予測不能なプログラムの動作、クラッシュ、または不正な結果(例: データの破損)を引き起こす可能性があります。
- Goには、
go run -race
やgo test -race
のように-race
フラグを付けて実行することで、データ競合を検出する組み込みのレース検出器があります。
-
defer
ステートメント:defer
ステートメントは、それが含まれる関数がリターンする直前に実行される関数呼び出しをスケジュールします。- リソースのクリーンアップ(ファイルのクローズ、ロックの解放など)によく使用され、エラーが発生した場合でも確実にクリーンアップが行われるようにします。
技術的詳細
このコミットが修正するデータ競合は、benchmarkTCP
関数内で起動されるサーバー側のゴルーチンが、ベンチマークのメイン処理が終了した後も b.Logf
を呼び出す可能性があったことに起因します。
データ競合の具体的なシナリオ:
benchmarkTCP
関数は、TCPリスナーを起動し、numConcurrent
個の並行接続を処理するために複数のサーバーゴルーチンとクライアントゴルーチンを起動します。- サーバーゴルーチンは、クライアントからの接続を受け入れ、データを処理します。この処理中にネットワークエラーなどが発生する可能性があります。
- エラーが発生した場合、サーバーゴルーチンは
b.Logf
を呼び出してエラー情報をログに記録しようとします。 - 問題は、
benchmarkTCP
関数がb.N
回のイテレーションを完了し、ベンチマークのメイン処理が終了してtesting.B
オブジェクトがクリーンアップ段階に入った後でも、まだ一部のサーバーゴルーチンが実行中であり、エラーログのためにb.Logf
を呼び出す可能性があることです。 testing.B
オブジェクトはベンチマークのライフサイクルに厳密に結びついており、そのライフサイクル外でb.Logf
が呼び出されると、すでに解放されたメモリや不正な状態のオブジェクトにアクセスしようとすることになり、これがデータ競合として検出されます。
修正アプローチ:
このコミットは、クライアントゴルーチンとサーバーゴルーチンの両方が、benchmarkTCP
関数がリターンする前に確実に終了するように、チャネルを用いた同期メカニズムを導入することでこの問題を解決します。
-
serverSem
とclientSem
チャネルの導入:serverSem := make(chan bool, numConcurrent)
: サーバーゴルーチンの終了を待機するためのバッファ付きチャネル。numConcurrent
は同時に処理される接続の最大数です。このチャネルは、numConcurrent
個の「スロット」を持つセマフォのように機能します。clientSem := make(chan bool, numConcurrent)
: 同様に、クライアントゴルーチンの終了を待機するためのバッファ付きチャネル。
-
ゴルーチン起動時のチャネルへの送信:
- サーバーゴルーチンが起動される直前に
serverSem <- true
が実行されます。これは、サーバーゴルーチンが1つのスロットを「取得」したことを示します。 - クライアントゴルーチンが起動される直前に
clientSem <- true
が実行されます。これは、クライアントゴルーチンが1つのスロットを「取得」したことを示します。 - チャネルがバッファの容量に達すると、それ以上の送信はブロックされ、同時に実行されるゴルーチンの数が
numConcurrent
に制限されます。
- サーバーゴルーチンが起動される直前に
-
defer
を用いたチャネルからの受信(スロットの解放):- サーバーゴルーチンとクライアントゴルーチンの両方で、
defer
ステートメント内にチャネルからの受信操作 (<-serverSem
または<-clientSem
) が配置されます。 - これにより、各ゴルーチンが終了する際に必ず対応するチャネルから値を受信し、スロットを「解放」します。この解放されたスロットは、メイン関数がゴルーチンの終了を検知するために使用されます。
- サーバーゴルーチンとクライアントゴルーチンの両方で、
-
メイン関数での待機:
benchmarkTCP
関数の最後に、以下のループが追加されました。for i := 0; i < numConcurrent; i++ { clientSem <- true serverSem <- true }
- このループは、
numConcurrent
回繰り返されます。各イテレーションでclientSem
とserverSem
に値を送信しようとします。 - チャネルはバッファ付きであり、ゴルーチンが終了する際にチャネルから値を受信してスロットを解放するため、このループは、すべてのクライアントゴルーチンとサーバーゴルーチンが終了し、それぞれのチャネルに送信されたすべてのトークンが消費されるまで、メインゴルーチンをブロックします。
- これにより、
benchmarkTCP
関数がリターンする前に、すべてのバックグラウンドゴルーチンが確実に終了することが保証されます。結果として、b.Logf
がtesting.B
オブジェクトの有効なライフサイクル外で呼び出される可能性がなくなり、データ競合が解消されます。
この修正により、ベンチマークの信頼性が向上し、並行処理における潜在的な競合状態が適切に管理されるようになりました。
コアとなるコードの変更箇所
src/pkg/net/tcp_test.go
における主要な変更点は以下の通りです。
--- a/src/pkg/net/tcp_test.go
+++ b/src/pkg/net/tcp_test.go
@@ -97,6 +97,7 @@ func benchmarkTCP(b *testing.B, persistent, timeout bool, laddr string) {
b.Fatalf("Listen failed: %v", err)
}
defer ln.Close()
+ serverSem := make(chan bool, numConcurrent) // サーバーゴルーチンの同期用チャネルを追加
// Acceptor.
go func() {
for {
@@ -104,9 +105,13 @@ func benchmarkTCP(b *testing.B, persistent, timeout bool, laddr string) {
if err != nil {
break
}
+ serverSem <- true // サーバーゴルーチン開始時にチャネルに送信
// Server connection.
go func(c Conn) {
- defer c.Close()
+ defer func() { // defer ブロックを変更し、チャネルからの受信を追加
+ c.Close()
+ <-serverSem // サーバーゴルーチン終了時にチャネルから受信
+ }()
if timeout {
c.SetDeadline(time.Now().Add(time.Hour)) // Not intended to fire.
}
@@ -119,13 +124,13 @@ func benchmarkTCP(b *testing.B, persistent, timeout bool, laddr string) {
}(c)
}
}()
- sem := make(chan bool, numConcurrent) // クライアントゴルーチンの同期用チャネル名を変更
- for i := 0; i < conns; i++ {
- sem <- true
+ clientSem := make(chan bool, numConcurrent) // クライアントゴルーチンの同期用チャネル名を変更
+ for i := 0; i < conns; i++ {
+ clientSem <- true // クライアントゴルーチン開始時にチャネルに送信
// Client connection.
go func() {
defer func() {
- <-sem // クライアントゴルーチン終了時にチャネルから受信
+ <-clientSem // クライアントゴルーチン終了時にチャネルから受信
}()
c, err := Dial("tcp", ln.Addr().String())
if err != nil {
@@ -144,8 +149,9 @@ func benchmarkTCP(b *testing.B, persistent, timeout bool, laddr string) {
}
}()
}
- for i := 0; i < cap(sem); i++ { // 最後の同期ループを変更
- sem <- true
+ for i := 0; i < numConcurrent; i++ { // 最後の同期ループを変更
+ clientSem <- true // すべてのクライアントゴルーチンが終了するまで待機
+ serverSem <- true // すべてのサーバーゴルーチンが終了するまで待機
}
}
コアとなるコードの解説
このコミットの核心は、serverSem
と clientSem
という2つのバッファ付きチャネルを導入し、これらを使ってサーバーとクライアントのゴルーチンのライフサイクルを benchmarkTCP
関数のメイン処理と同期させる点にあります。
-
serverSem := make(chan bool, numConcurrent)
の追加:- これは、同時に実行されるサーバーゴルーチンの数を
numConcurrent
に制限し、それらの終了を追跡するためのセマフォとして機能します。
- これは、同時に実行されるサーバーゴルーチンの数を
-
サーバーゴルーチン内での変更:
serverSem <- true
: 新しいサーバーゴルーチンが起動される直前にこの行が追加されました。これにより、チャネルにブール値が送信され、numConcurrent
個の利用可能な「スロット」の1つが消費されます。チャネルが満杯の場合、この送信操作はブロックされ、同時に実行されるサーバーゴルーチンの数が制限されます。defer func() { c.Close(); <-serverSem }()
: サーバーゴルーチンが終了する際に実行されるdefer
関数が変更されました。以前は単に接続をクローズするだけでしたが、<-serverSem
が追加されました。これは、チャネルから値を受信することで、消費されていたスロットを「解放」します。これにより、メイン関数は、このゴルーチンが正常に終了したことを検知できるようになります。
-
クライアントゴルーチン内での変更:
sem
チャネルの名前がclientSem
に変更されました。機能的にはserverSem
と同様に、クライアントゴルーチンの数を制御し、終了を追跡します。clientSem <- true
: クライアントゴルーチンが起動される直前にこの行が追加され、クライアント側のスロットを消費します。defer func() { <-clientSem }()
: クライアントゴルーチンが終了する際に実行されるdefer
関数が変更され、<-clientSem
が追加されました。これにより、クライアントゴルーチンが終了した際にスロットが解放されます。
-
benchmarkTCP
関数の最後の同期ループ:for i := 0; i < numConcurrent; i++ { clientSem <- true; serverSem <- true }
: このループは、benchmarkTCP
関数のメイン処理が終了する直前に追加されました。- このループは、
numConcurrent
回、clientSem
とserverSem
の両方に値を送信しようとします。 - 各チャネルは
numConcurrent
のバッファ容量を持っているため、このループが正常に完了するためには、numConcurrent
個のクライアントゴルーチンとnumConcurrent
個のサーバーゴルーチンがそれぞれ終了し、チャネルから値を「受信」してスロットを解放している必要があります。 - もし、まだ実行中のゴルーチンがあり、チャネルに送信されたトークンがすべて消費されていない場合、このループの送信操作はブロックされます。
- 結果として、このループは、すべてのクライアントゴルーチンとサーバーゴルーチンが完全に終了するまで
benchmarkTCP
関数がリターンするのを待機することを保証します。これにより、b.Logf
がtesting.B
オブジェクトの有効なライフサイクル外で呼び出される可能性が完全に排除され、データ競合が解消されます。
この同期メカニズムにより、ベンチマークの実行中にバックグラウンドで動作するすべてのゴルーチンが、ベンチマークの終了前に適切にクリーンアップされることが保証され、テストの信頼性と安定性が向上します。
関連リンク
- Go CL (Code Review): https://golang.org/cl/84750047
- 関連Issue: https://golang.org/issue/7718
- (注: このIssueの詳細は、一般的なGoのIssueトラッカーでは直接見つからない可能性がありますが、コミットメッセージに明示的に記載されています。)
参考にした情報源リンク
- Go言語公式ドキュメント -
testing
パッケージ: - Go言語公式ドキュメント -
net
パッケージ: - Go言語公式ドキュメント - Concurrency (Goroutines and Channels):
- https://go.dev/tour/concurrency/1
- https://go.dev/blog/pipelines (チャネルのより高度な使用例)
- Go Race Detectorに関する情報:
- Goにおけるチャネルを用いたゴルーチン同期の一般的なパターン:
- (Web検索で得られた一般的なGoのチャネル同期に関する記事やチュートリアル)
- 例: https://gobyexample.com/channels (Go by Example - Channels)
- 例: https://gobyexample.com/channel-synchronization (Go by Example - Channel Synchronization)