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

[インデックス 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.Logftesting.B オブジェクトの有効なライフサイクル内でのみ呼び出されることが保証されます。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語の概念とテストフレームワークに関する知識が役立ちます。

  1. GoのnetパッケージとTCP通信:

    • net パッケージは、ネットワークI/Oプリミティブを提供するGoの標準ライブラリです。TCP/IP、UDP、Unixドメインソケットなどのネットワークプログラミングをサポートします。
    • net.Listen: 指定されたネットワークアドレスで着信接続をリッスンするためのリスナーを作成します。TCPサーバーの開始点となります。
    • net.Dial: 指定されたネットワークアドレスへの接続を確立します。TCPクライアントの開始点となります。
    • net.Conn: ネットワーク接続を表すインターフェースで、データの読み書き (Read, Write) や接続のクローズ (Close) などのメソッドを提供します。
  2. 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(...): 致命的なエラーを報告し、ベンチマークを停止します。
  3. Goのゴルーチン (Goroutine) とチャネル (Channel):

    • ゴルーチン: Goの並行処理の基本単位です。非常に軽量なスレッドのようなもので、go キーワードを使って関数呼び出しの前に置くことで簡単に起動できます (例: go myFunction())。
    • チャネル: ゴルーチン間で安全にデータを送受信するための通信メカニズムです。チャネルは、Goの「メモリを共有して通信するのではなく、通信によってメモリを共有する」という並行処理の哲学を体現しています。
      • make(chan Type): バッファなしチャネルを作成します。送信操作は受信操作が準備できるまでブロックし、受信操作は送信操作が準備できるまでブロックします。これにより、厳密な同期ポイントが作成されます。
      • make(chan Type, capacity): バッファありチャネルを作成します。capacity で指定された数の要素をバッファに格納できます。バッファが満杯でない限り送信はブロックせず、バッファが空でない限り受信はブロックしません。セマフォやワークキューの実装によく使われます。
      • ch <- value: チャネル chvalue を送信します。
      • value := <-ch: チャネル ch から値を受信します。
  4. データ競合 (Data Race):

    • 複数のゴルーチンが同時に同じメモリ領域にアクセスし、そのうち少なくとも1つのアクセスが書き込み操作である場合に、適切な同期なしで発生する競合状態です。
    • データ競合は、予測不能なプログラムの動作、クラッシュ、または不正な結果(例: データの破損)を引き起こす可能性があります。
    • Goには、go run -racego test -race のように -race フラグを付けて実行することで、データ競合を検出する組み込みのレース検出器があります。
  5. defer ステートメント:

    • defer ステートメントは、それが含まれる関数がリターンする直前に実行される関数呼び出しをスケジュールします。
    • リソースのクリーンアップ(ファイルのクローズ、ロックの解放など)によく使用され、エラーが発生した場合でも確実にクリーンアップが行われるようにします。

技術的詳細

このコミットが修正するデータ競合は、benchmarkTCP 関数内で起動されるサーバー側のゴルーチンが、ベンチマークのメイン処理が終了した後も b.Logf を呼び出す可能性があったことに起因します。

データ競合の具体的なシナリオ:

  1. benchmarkTCP 関数は、TCPリスナーを起動し、numConcurrent 個の並行接続を処理するために複数のサーバーゴルーチンとクライアントゴルーチンを起動します。
  2. サーバーゴルーチンは、クライアントからの接続を受け入れ、データを処理します。この処理中にネットワークエラーなどが発生する可能性があります。
  3. エラーが発生した場合、サーバーゴルーチンは b.Logf を呼び出してエラー情報をログに記録しようとします。
  4. 問題は、benchmarkTCP 関数が b.N 回のイテレーションを完了し、ベンチマークのメイン処理が終了して testing.B オブジェクトがクリーンアップ段階に入った後でも、まだ一部のサーバーゴルーチンが実行中であり、エラーログのために b.Logf を呼び出す可能性があることです。
  5. testing.B オブジェクトはベンチマークのライフサイクルに厳密に結びついており、そのライフサイクル外で b.Logf が呼び出されると、すでに解放されたメモリや不正な状態のオブジェクトにアクセスしようとすることになり、これがデータ競合として検出されます。

修正アプローチ:

このコミットは、クライアントゴルーチンとサーバーゴルーチンの両方が、benchmarkTCP 関数がリターンする前に確実に終了するように、チャネルを用いた同期メカニズムを導入することでこの問題を解決します。

  1. serverSemclientSem チャネルの導入:

    • serverSem := make(chan bool, numConcurrent): サーバーゴルーチンの終了を待機するためのバッファ付きチャネル。numConcurrent は同時に処理される接続の最大数です。このチャネルは、numConcurrent 個の「スロット」を持つセマフォのように機能します。
    • clientSem := make(chan bool, numConcurrent): 同様に、クライアントゴルーチンの終了を待機するためのバッファ付きチャネル。
  2. ゴルーチン起動時のチャネルへの送信:

    • サーバーゴルーチンが起動される直前に serverSem <- true が実行されます。これは、サーバーゴルーチンが1つのスロットを「取得」したことを示します。
    • クライアントゴルーチンが起動される直前に clientSem <- true が実行されます。これは、クライアントゴルーチンが1つのスロットを「取得」したことを示します。
    • チャネルがバッファの容量に達すると、それ以上の送信はブロックされ、同時に実行されるゴルーチンの数が numConcurrent に制限されます。
  3. defer を用いたチャネルからの受信(スロットの解放):

    • サーバーゴルーチンとクライアントゴルーチンの両方で、defer ステートメント内にチャネルからの受信操作 (<-serverSem または <-clientSem) が配置されます。
    • これにより、各ゴルーチンが終了する際に必ず対応するチャネルから値を受信し、スロットを「解放」します。この解放されたスロットは、メイン関数がゴルーチンの終了を検知するために使用されます。
  4. メイン関数での待機:

    • benchmarkTCP 関数の最後に、以下のループが追加されました。
      for i := 0; i < numConcurrent; i++ {
          clientSem <- true
          serverSem <- true
      }
      
    • このループは、numConcurrent 回繰り返されます。各イテレーションで clientSemserverSem に値を送信しようとします。
    • チャネルはバッファ付きであり、ゴルーチンが終了する際にチャネルから値を受信してスロットを解放するため、このループは、すべてのクライアントゴルーチンとサーバーゴルーチンが終了し、それぞれのチャネルに送信されたすべてのトークンが消費されるまで、メインゴルーチンをブロックします。
    • これにより、benchmarkTCP 関数がリターンする前に、すべてのバックグラウンドゴルーチンが確実に終了することが保証されます。結果として、b.Logftesting.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 // すべてのサーバーゴルーチンが終了するまで待機
 	}
 }

コアとなるコードの解説

このコミットの核心は、serverSemclientSem という2つのバッファ付きチャネルを導入し、これらを使ってサーバーとクライアントのゴルーチンのライフサイクルを benchmarkTCP 関数のメイン処理と同期させる点にあります。

  1. serverSem := make(chan bool, numConcurrent) の追加:

    • これは、同時に実行されるサーバーゴルーチンの数を numConcurrent に制限し、それらの終了を追跡するためのセマフォとして機能します。
  2. サーバーゴルーチン内での変更:

    • serverSem <- true: 新しいサーバーゴルーチンが起動される直前にこの行が追加されました。これにより、チャネルにブール値が送信され、numConcurrent 個の利用可能な「スロット」の1つが消費されます。チャネルが満杯の場合、この送信操作はブロックされ、同時に実行されるサーバーゴルーチンの数が制限されます。
    • defer func() { c.Close(); <-serverSem }(): サーバーゴルーチンが終了する際に実行される defer 関数が変更されました。以前は単に接続をクローズするだけでしたが、<-serverSem が追加されました。これは、チャネルから値を受信することで、消費されていたスロットを「解放」します。これにより、メイン関数は、このゴルーチンが正常に終了したことを検知できるようになります。
  3. クライアントゴルーチン内での変更:

    • sem チャネルの名前が clientSem に変更されました。機能的には serverSem と同様に、クライアントゴルーチンの数を制御し、終了を追跡します。
    • clientSem <- true: クライアントゴルーチンが起動される直前にこの行が追加され、クライアント側のスロットを消費します。
    • defer func() { <-clientSem }(): クライアントゴルーチンが終了する際に実行される defer 関数が変更され、<-clientSem が追加されました。これにより、クライアントゴルーチンが終了した際にスロットが解放されます。
  4. benchmarkTCP 関数の最後の同期ループ:

    • for i := 0; i < numConcurrent; i++ { clientSem <- true; serverSem <- true }: このループは、benchmarkTCP 関数のメイン処理が終了する直前に追加されました。
    • このループは、numConcurrent 回、clientSemserverSem の両方に値を送信しようとします。
    • 各チャネルは numConcurrent のバッファ容量を持っているため、このループが正常に完了するためには、numConcurrent 個のクライアントゴルーチンと numConcurrent 個のサーバーゴルーチンがそれぞれ終了し、チャネルから値を「受信」してスロットを解放している必要があります。
    • もし、まだ実行中のゴルーチンがあり、チャネルに送信されたトークンがすべて消費されていない場合、このループの送信操作はブロックされます。
    • 結果として、このループは、すべてのクライアントゴルーチンとサーバーゴルーチンが完全に終了するまで benchmarkTCP 関数がリターンするのを待機することを保証します。これにより、b.Logftesting.B オブジェクトの有効なライフサイクル外で呼び出される可能性が完全に排除され、データ競合が解消されます。

この同期メカニズムにより、ベンチマークの実行中にバックグラウンドで動作するすべてのゴルーチンが、ベンチマークの終了前に適切にクリーンアップされることが保証され、テストの信頼性と安定性が向上します。

関連リンク

  • Go CL (Code Review): https://golang.org/cl/84750047
  • 関連Issue: https://golang.org/issue/7718
    • (注: このIssueの詳細は、一般的なGoのIssueトラッカーでは直接見つからない可能性がありますが、コミットメッセージに明示的に記載されています。)

参考にした情報源リンク