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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージのテストファイル src/pkg/net/http/fs_test.go におけるデータ競合(data race)を修正するものです。具体的には、TestServeFileContentType というテスト関数内で発生していた競合状態を、bool 型の変数からチャネル(channel)を用いた同期メカニズムに変更することで解決しています。

コミット

commit 92686dda7c76e574d0a7fa447233e2ea7fd6ad59
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Jan 16 14:47:33 2012 +0400

    net/http: fix data race in test
    Fixes #2712.

    R=golang-dev, dsymonds
    CC=golang-dev, mpimenov
    https://golang.org/cl/5543062

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

https://github.com/golang/go/commit/92686dda7c76e574d0a7fa447233e2ea7fd6ad59

元コミット内容

net/http: fix data race in test
Fixes #2712.

R=golang-dev, dsymonds
CC=golang-dev, mpimenov
https://golang.org/cl/5543062

変更の背景

このコミットは、Go言語の net/http パッケージのテストコード fs_test.go 内で発生していたデータ競合を修正するために行われました。データ競合は、複数のゴルーチン(goroutine)が同時に同じメモリ領域にアクセスし、そのうち少なくとも1つが書き込み操作である場合に発生する並行処理のバグです。このような競合は、テストの実行結果を不安定にしたり、予測不能な動作を引き起こしたりする可能性があります。

具体的には、TestServeFileContentType というテスト関数において、override という bool 型の変数が、HTTPハンドラ内で読み取られ、テスト関数本体で書き込まれるという状況がありました。Goのテストは並行して実行される可能性があるため、この override 変数への非同期なアクセスがデータ競合を引き起こしていました。GoのIssue #2712で報告された問題に対応するものです。

前提知識の解説

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

Go言語は、並行処理を言語レベルで強力にサポートしています。

  • ゴルーチン (Goroutine): Goにおける軽量な実行スレッドです。関数呼び出しの前に go キーワードを付けるだけで、その関数は新しいゴルーチンとして並行に実行されます。ゴルーチンはOSのスレッドよりもはるかに軽量であり、数千、数万のゴルーチンを同時に実行することが可能です。
  • チャネル (Channel): ゴルーチン間の通信と同期のための主要なメカニズムです。チャネルは、あるゴルーチンから別のゴルーチンへ値を安全に送信・受信するためのパイプのようなものです。Goの並行処理の哲学は「共有メモリを通信によって共有するのではなく、通信によってメモリを共有する (Do not communicate by sharing memory; instead, share memory by communicating.)」というものであり、チャネルはこの哲学を具現化するものです。チャネルは、データ競合を避けるための安全な通信手段を提供します。

データ競合 (Data Race)

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

  1. 複数のゴルーチンが同時に同じメモリ領域にアクセスする。
  2. そのメモリ領域が共有変数である。
  3. 少なくとも1つのアクセスが書き込み操作である。

データ競合が発生すると、プログラムの動作が予測不能になり、誤った結果、データ破損、クラッシュなどを引き起こす可能性があります。Goには、実行時にデータ競合を検出するための「Race Detector」が組み込まれており、go run -racego test -race コマンドで有効にすることができます。

net/http パッケージの httptest.NewServerServeFile

  • httptest.NewServer: net/http/httptest パッケージに含まれる関数で、HTTPサーバーのテストを容易にするために使用されます。この関数は、指定された http.Handler を持つ新しいHTTPテストサーバーを起動し、そのサーバーのURLを返します。これにより、実際のネットワークリクエストをシミュレートして、HTTPクライアントやハンドラの動作をテストできます。テストサーバーは、テストが終了すると自動的にクリーンアップされます。
  • http.ServeFile: net/http パッケージに含まれる関数で、指定されたファイルの内容をHTTPレスポンスとしてクライアントに提供します。ファイルのMIMEタイプ(Content-Type)は、ファイル拡張子に基づいて自動的に推測されますが、ResponseWriterHeader().Set("Content-Type", ...) を使って明示的に上書きすることも可能です。

Content-Type ヘッダ

HTTPの Content-Type ヘッダは、HTTPメッセージのボディに含まれるデータのメディアタイプ(MIMEタイプ)を示します。例えば、text/html はHTMLドキュメント、application/json はJSONデータ、image/png はPNG画像などを表します。クライアントは Content-Type ヘッダを見て、受信したデータをどのように解釈・表示すべきかを判断します。

技術的詳細

このコミットの核心は、データ競合を引き起こしていた bool 型の共有変数 override を、Goの並行処理のベストプラクティスであるチャネルに置き換えることです。

元のコードでは、override は単なる bool 型の変数でした。

override := false

この変数は、httptest.NewServer で起動されるHTTPハンドラ(別のゴルーチンで実行される)と、テスト関数本体(メインのゴルーチンで実行される)の両方からアクセスされていました。

  • HTTPハンドラ内: if override { ... } のように override の値を読み取っていました。
  • テスト関数本体: override = trueoverride = false のように override の値を書き換えていました。

Goのテストは並行して実行される可能性があるため、これらの読み書きが同時に発生するとデータ競合が発生します。

修正後のコードでは、override をバッファ付きチャネル (chan bool) に変更しています。

override := make(chan bool, 1)
  • make(chan bool, 1): これは、bool 型の値を1つだけ格納できるバッファ付きチャネルを作成します。バッファ付きチャネルは、送信側が受信側を待つことなく、バッファが満杯になるまで値を送信できる特性があります。この場合、バッファサイズが1なので、チャネルに1つの値が送信されると、次の送信は受信されるまでブロックされます。

変更点とデータ競合の解決メカニズムは以下の通りです。

  1. override 変数の型変更:

    • override := false (bool型)
    • override := make(chan bool, 1) (chan bool型) これにより、override へのアクセスがチャネル操作(送信と受信)に置き換わります。チャネル操作はGoランタイムによって同期が保証されているため、データ競合が発生しません。
  2. HTTPハンドラ内の変更:

    • if override {
    • if <-override { <-override はチャネルからの受信操作です。HTTPハンドラが実行されるたびに、このチャネルから値を受信しようとします。チャネルに値が送信されるまで、ハンドラはブロックされます。これにより、ハンドラが override の値を読み取るタイミングが、テスト関数本体が値を送信するタイミングと同期されます。
  3. テスト関数本体内の変更:

    • override = false
    • override = true
    • override <- false
    • override <- true override <- value はチャネルへの送信操作です。テスト関数本体は、get 関数を呼び出す前にチャネルに値を送信します。これにより、HTTPハンドラがその値を受信し、適切な Content-Type ヘッダを設定するよう制御されます。

この変更により、override 変数へのアクセスは、チャネルを介した同期された通信に置き換えられます。チャネルはGoの並行処理プリミティブであり、それ自体がデータ競合フリーな操作を保証します。したがって、複数のゴルーチンが同時に override にアクセスしても、競合状態は発生しなくなります。バッファサイズが1であるため、常に最新の override の状態がハンドラに伝達されることが保証されます。

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

--- a/src/pkg/net/http/fs_test.go
+++ b/src/pkg/net/http/fs_test.go
@@ -224,9 +224,9 @@ func TestEmptyDirOpenCWD(t *testing.T) {
 
 func TestServeFileContentType(t *testing.T) {
 	const ctype = "icecream/chocolate"
-	override := false
-	override := make(chan bool, 1)
+	override := make(chan bool, 1)
 	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
-		if override {
+		if <-override {
 			w.Header().Set("Content-Type", ctype)
 		}
 		ServeFile(w, r, "testdata/file")
@@ -241,8 +241,9 @@ func TestServeFileContentType(t *testing.T) {
 			t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
 		}
 	}
+	override <- false
 	get("text/plain; charset=utf-8")
-	override = true
+	override <- true
 	get(ctype)
 }

コアとなるコードの解説

  1. override 変数の初期化の変更:

    -	override := false
    +	override := make(chan bool, 1)
    

    override 変数の型が bool から chan bool (ブール値を送受信するチャネル) に変更されました。make(chan bool, 1) は、バッファサイズが1のチャネルを作成します。これにより、チャネルに1つの値が送信されると、次の送信は受信されるまでブロックされます。これは、テストの各ステップで override の状態を確実に同期させるために重要です。

  2. HTTPハンドラ内の override の使用方法の変更:

    -		if override {
    +		if <-override {
    

    HTTPハンドラ内で override の値を直接参照する代わりに、<-override を使用してチャネルから値を受信するように変更されました。これにより、ハンドラが Content-Type を上書きするかどうかを決定する前に、テスト関数本体がチャネルに値を送信するのを待つようになります。チャネルからの受信はブロッキング操作であるため、ハンドラはテスト関数本体からの明示的な指示があるまで処理を進めません。これにより、ハンドラが古い override の値を見てしまうデータ競合が解消されます。

  3. テスト関数本体内の override の設定方法の変更:

    +	override <- false
     	get("text/plain; charset=utf-8")
    -	override = true
    +	override <- true
     	get(ctype)
    

    get 関数を呼び出す前に、override <- false および override <- true を使用してチャネルに値を送信するように変更されました。これにより、HTTPハンドラが <-override で受信する値が、テストの現在のフェーズで意図された Content-Type の上書き状態を正確に反映するようになります。チャネルを介した明示的な通信により、テスト関数本体とHTTPハンドラ間の同期が確立され、データ競合が完全に排除されます。

関連リンク

参考にした情報源リンク