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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージ内のベンチマークテストにおけるデータ競合(data race)を修正するものです。具体的には、serve_test.go ファイル内のベンチマーク関数 BenchmarkServe において、Serve 関数が誤って新しいゴルーチンで起動されていたために発生していた競合状態を解消します。

コミット

commit 1fa0206024e543b78e2ffea7997a0ac676cec708
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Dec 26 12:16:11 2013 -0800

    net/http: fix data race in benchmark
    
    Fixes #7006
    
    R=golang-codereviews, iant
    CC=golang-codereviews
    https://golang.org/cl/44940044

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

https://github.com/golang/go/commit/1fa0206024e543b78e2ffea7997a0ac676cec708

元コミット内容

このコミットの元の内容は、「net/http: ベンチマークにおけるデータ競合を修正する」というものです。これは、net/http パッケージのテストコードに存在するデータ競合の問題を解決することを目的としています。コミットメッセージには Fixes #7006 と記載されており、これはGoプロジェクトの内部的な課題追跡システムにおける特定の課題番号を示唆していますが、現在の公開されているGoのIssue Trackerではこの番号の課題は直接見つかりませんでした。これは、Issue Trackerの移行や、内部的な課題管理番号である可能性が考えられます。

変更の背景

この変更の背景には、Go言語の並行処理モデルにおける「データ競合」という重要な問題があります。データ競合は、複数のゴルーチン(Goの軽量スレッド)が同時に同じメモリ領域にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生します。このような状況では、実行のタイミングによって結果が非決定論的になり、予期せぬバグやクラッシュを引き起こす可能性があります。

net/http パッケージの serve_test.go 内のベンチマークテスト BenchmarkServe は、HTTPサーバーのパフォーマンスを測定するために設計されています。しかし、このベンチマークの実装において、Serve 関数が go キーワードを使って新しいゴルーチンで起動されていました。ベンチマークの目的は、Serve 関数の単一の実行パスのパフォーマンスを測定することであり、その実行が完了するのを待つ必要があります。しかし、go Serve(ln, h) とすることで、Serve 関数は非同期に実行され、ベンチマークのメインゴルーチンは Serve の完了を待たずに次のイテレーションに進んでしまう可能性がありました。

この非同期実行と、ベンチマーク内で共有される conn オブジェクト(ln.conn = conn の行で設定される)へのアクセスが重なることで、データ競合が発生していました。具体的には、Serve ゴルーチンが conn を使用している最中に、ベンチマークのメインゴルーチンが次のイテレーションで conn をリセットしようとする、といった状況が考えられます。このような競合は、ベンチマーク結果の信頼性を損なうだけでなく、テスト実行時にランタイムエラーを発生させる可能性もありました。

このコミットは、このデータ競合を特定し、Serve 関数を同期的に実行するように変更することで、ベンチマークの正確性と安定性を確保することを目的としています。

前提知識の解説

Go言語の並行処理(GoroutinesとChannels)

Go言語は、並行処理を言語レベルでサポートしており、その中心となるのが「ゴルーチン(Goroutine)」と「チャネル(Channel)」です。

  • ゴルーチン (Goroutine): Goランタイムによって管理される軽量な実行スレッドです。関数呼び出しの前に go キーワードを付けるだけで、その関数は新しいゴルーチンとして並行に実行されます。ゴルーチンはOSのスレッドよりもはるかに軽量であり、数千、数万のゴルーチンを同時に実行することが可能です。
  • チャネル (Channel): ゴルーチン間で安全にデータを送受信するための通信メカニズムです。チャネルは、Goの「共有メモリによる通信ではなく、通信による共有メモリ」という並行処理の哲学を体現しています。チャネルを使用することで、データ競合を避けてゴルーチン間で同期を取りながらデータをやり取りできます。

データ競合 (Data Race)

データ競合は、並行プログラミングにおける一般的なバグの一種です。以下の3つの条件がすべて満たされたときに発生します。

  1. 複数のゴルーチンが同時に同じメモリ領域にアクセスする。
  2. 少なくとも1つのアクセスが書き込み操作である。
  3. それらのアクセスが同期メカニズム(ミューテックス、チャネルなど)によって保護されていない。

データ競合が発生すると、プログラムの動作が非決定論的になり、デバッグが非常に困難になります。Go言語には、データ競合を検出するための「競合検出器(Race Detector)」が組み込まれており、go run -racego test -race のように -race フラグを付けて実行することで、競合を検出できます。

net/http パッケージと http.Serve 関数

net/http パッケージは、Go言語でHTTPクライアントとサーバーを構築するための標準ライブラリです。

  • http.Serve(l net.Listener, handler Handler): この関数は、指定された net.Listener からの新しい接続を受け入れ、それぞれの接続に対して新しいゴルーチンを起動し、そのゴルーチン内で handler を呼び出してHTTPリクエストを処理します。通常、HTTPサーバーを起動する際に使用されます。

Goのベンチマークテスト

Go言語には、コードのパフォーマンスを測定するためのベンチマークテスト機能が組み込まれています。ベンチマーク関数は BenchmarkXxx(*testing.B) というシグネチャを持ち、b.N 回のループ内で測定対象のコードを実行します。b.N は、ベンチマークランナーが自動的に調整するイテレーション回数です。

技術的詳細

このコミットの技術的な核心は、go キーワードの有無がベンチマークの動作に与える影響と、それによって引き起こされるデータ競合のメカニズムにあります。

元のコードでは、Serve 関数が go Serve(ln, h) のように新しいゴルーチンで起動されていました。これは、通常のHTTPサーバーでは一般的なパターンです。http.Serve 関数自体が内部で各接続を処理するために新しいゴルーチンを起動しますが、ここでは Serve 関数全体をさらに別のゴルーチンで実行していました。

ベンチマークのコンテキストでは、BenchmarkServe 関数は b.N 回のループで Serve の実行を測定しようとしています。

// 元のコード
for i := 0; i < b.N; i++ {
    conn.Reader = bytes.NewReader(req)
    ln.conn = conn
    go Serve(ln, h) // ここが問題
    <-conn.closec
}

ここで問題となるのは、go Serve(ln, h) が非同期に実行されることです。Serve 関数が新しいゴルーチンで起動されると、BenchmarkServe のメインゴルーチンはすぐに次の行 <-conn.closec に進みます。conn.closecServe ゴルーチンが接続の処理を終えたときにシグナルを送るチャネルですが、Serve ゴルーチンが完全に終了する前に、メインゴルーチンが次のループイテレーションに入ってしまう可能性がありました。

次のイテレーションに入ると、conn.Reader = bytes.NewReader(req)ln.conn = conn の行で、conn オブジェクトが再利用または上書きされます。もし前のイテレーションで起動された Serve ゴルーチンがまだ conn オブジェクトを使用している最中に、メインゴルーチンが conn を変更してしまうと、データ競合が発生します。これは、複数のゴルーチンが同期なしに同じ conn オブジェクト(特にその内部状態)にアクセスし、少なくとも一方が書き込みを行うためです。

修正後のコードでは、go キーワードが削除され、Serve(ln, h) と直接呼び出されています。

// 修正後のコード
for i := 0; i < b.N; i++ {
    conn.Reader = bytes.NewReader(req)
    ln.conn = conn
    Serve(ln, h) // 修正箇所
    <-conn.closec
}

これにより、Serve 関数は BenchmarkServe のメインゴルーチン内で同期的に実行されるようになります。Serve 関数が完了するまで、メインゴルーチンは次の行に進みません。したがって、conn オブジェクトが次のイテレーションで再利用される前に、前のイテレーションでの Serve の実行が完全に終了することが保証されます。これにより、conn オブジェクトに対する同時アクセスがなくなり、データ競合が解消されます。

この変更は、ベンチマークの目的(単一の Serve 呼び出しのパフォーマンス測定)に合致しており、テストの信頼性と安定性を向上させます。

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

--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -2436,7 +2436,7 @@ Host: golang.org
 	for i := 0; i < b.N; i++ {\n \t\tconn.Reader = bytes.NewReader(req)\n \t\tln.conn = conn\n-\t\tgo Serve(ln, h)\n+\t\tServe(ln, h)\n \t\t<-conn.closec\n \t}\n }\n```

## コアとなるコードの解説

変更は `src/pkg/net/http/serve_test.go` ファイルの `BenchmarkServe` 関数内の一行です。

*   **`- go Serve(ln, h)`**: 変更前のコードでは、`Serve` 関数が `go` キーワードを使って新しいゴルーチンで起動されていました。これにより、`Serve` の実行は非同期になり、ベンチマークのメインゴルーチンは `Serve` の完了を待たずに次の処理(`<-conn.closec` や次のループイテレーション)に進む可能性がありました。これが、`conn` オブジェクトへの同時アクセスによるデータ競合の原因となっていました。
*   **`+ Serve(ln, h)`**: 変更後のコードでは、`go` キーワードが削除され、`Serve` 関数が直接呼び出されています。これにより、`Serve` 関数はベンチマークのメインゴルーチン内で同期的に実行されます。`Serve` 関数が完全に終了するまで、メインゴルーチンは次の行に進みません。この修正により、`conn` オブジェクトが次のイテレーションで再利用される前に、前のイテレーションでの `Serve` の実行が確実に完了するため、データ競合が解消されます。

このシンプルな変更は、Goの並行処理における `go` キーワードのセマンティクスと、ベンチマークテストにおける正確な測定の重要性を浮き彫りにしています。

## 関連リンク

*   Go言語の公式ドキュメント: [https://golang.org/doc/](https://golang.org/doc/)
*   Go言語の並行処理に関する公式ブログ記事: [https://go.dev/blog/concurrency-is-not-parallelism](https://go.dev/blog/concurrency-is-not-parallelism)
*   Go言語の競合検出器に関するドキュメント: [https://go.dev/doc/articles/race_detector](https://go.dev/doc/articles/race_detector)

## 参考にした情報源リンク

*   Go言語の公式ドキュメント
*   Go言語のソースコード(`net/http` パッケージおよび `testing` パッケージ)
*   データ競合に関する一般的なプログラミングの概念
*   Go言語のベンチマークに関する情報