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

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

このコミットは、Go言語の標準ライブラリ net/http パッケージにおけるデータ競合(data race)の修正に関するものです。具体的には、countReader 構造体の n フィールドに対する並行アクセスによって発生する競合状態を解消し、TestRequestBodyLimit テストの信頼性を向上させています。

コミット

commit a14f87ca81682ffd0134bf25e32b874dbd1d0757
Author: Dave Cheney <dave@cheney.net>
Date:   Fri Oct 12 09:17:56 2012 +1100

    net/http: fix data race on countReader.n
    
    Fixes #4220.
    
    R=dvyukov, bradfitz
    CC=golang-dev
    https://golang.org/cl/6638053

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

https://github.com/golang/go/commit/a14f87ca81682ffd0134bf25e32b874dbd1d0757

元コミット内容

net/http: fix data race on countReader.n
Fixes #4220.
R=dvyukov, bradfitz
CC=golang-dev
https://golang.org/cl/6638053

変更の背景

このコミットは、Go言語のIssue #4220を修正するために行われました。Issue #4220では、net/http パッケージのテストコード serve_test.go 内の countReader 構造体において、n フィールドへのアクセスが並行処理環境下でデータ競合を引き起こす可能性が指摘されていました。

countReaderio.Reader インターフェースを実装しており、読み込んだバイト数をカウントするために n という *int64 型のポインタを保持しています。Read メソッド内で *cr.n += int64(n) という操作が行われていましたが、この操作は「読み込み」「加算」「書き込み」という複数のステップから成り立っており、複数のGoroutineが同時にこの操作を実行しようとすると、中間状態が他のGoroutineから観測され、最終的な n の値が不正になる可能性がありました。これは典型的なデータ競合のシナリオです。

このデータ競合は、TestRequestBodyLimit のようなテストにおいて、正確なバイト数カウントを妨げ、テスト結果の信頼性を損なう可能性がありました。そのため、この競合状態を解消し、テストの正確性を保証する必要がありました。

前提知識の解説

データ競合 (Data Race)

データ競合とは、複数の並行実行されるGoroutine(またはスレッド)が、少なくとも1つが書き込み操作である共有メモリ上の同じ変数に同時にアクセスし、かつそのアクセスが同期メカニズムによって保護されていない場合に発生するプログラミング上のバグです。データ競合が発生すると、プログラムの動作が予測不能になり、クラッシュ、不正な結果、セキュリティ脆弱性など、様々な問題を引き起こす可能性があります。

Go言語では、データ競合を検出するためのツールとしてRace Detectorが提供されています。

アトミック操作 (Atomic Operations)

アトミック操作とは、不可分(これ以上分割できない)な操作のことです。つまり、その操作が実行されている間は、他のGoroutineからその操作の中間状態が観測されることはなく、完全に実行されるか、全く実行されないかのどちらかになります。これにより、複数のGoroutineが同時に同じデータにアクセスしても、データ競合が発生するのを防ぐことができます。

Go言語では、sync/atomic パッケージがアトミック操作を提供しています。

  • atomic.AddInt64(addr *int64, delta int64): addr が指す int64 型の値に delta をアトミックに加算します。この操作は、読み込み、加算、書き込みが単一のアトミックなステップとして実行されることを保証します。
  • atomic.LoadInt64(addr *int64) int64: addr が指す int64 型の値をアトミックに読み込みます。これにより、他のGoroutineによる書き込み操作の途中の値ではなく、完全に書き込まれた値が読み込まれることが保証されます。

io.Reader インターフェース

io.Reader はGo言語の標準ライブラリ io パッケージで定義されているインターフェースです。

type Reader interface {
    Read(p []byte) (n int, err error)
}

Read メソッドは、データを p に読み込み、読み込んだバイト数 n とエラー err を返します。nlen(p) 以下である必要があります。Readn > 0err == EOF を同時に返すことはありません。

io.LimitReader

io.LimitReader は、指定されたバイト数までしか読み込まない io.Reader を返します。これは、大きな入力ストリームから一部だけを読み込みたい場合や、入力のサイズを制限したい場合に便利です。

技術的詳細

このコミットの主要な変更点は、countReader 構造体の Read メソッド内での n フィールドへのアクセス方法と、TestRequestBodyLimit テストでの nWritten 変数の初期化および使用方法です。

countReader.Read メソッドの変更

変更前:

func (cr countReader) Read(p []byte) (n int, err error) {
	n, err = cr.r.Read(p)
	*cr.n += int64(n) // データ競合の可能性
	return
}

変更後:

func (cr countReader) Read(p []byte) (n int, err error) {
	n, err = cr.r.Read(p)
	atomic.AddInt64(cr.n, int64(n)) // アトミックな加算
	return
}

*cr.n += int64(n) は、cr.n が指す値の読み込み、int64(n) の加算、そして結果の書き込みという3つの操作から構成されます。これらの操作はCPU命令レベルでは複数の命令に分解されるため、複数のGoroutineが同時にこの行を実行すると、中間状態が他のGoroutineから観測され、最終的な値が正しくない可能性があります。

atomic.AddInt64(cr.n, int64(n)) を使用することで、この加算操作全体が不可分なものとして実行されることが保証されます。これにより、複数のGoroutineが同時に Read メソッドを呼び出しても、cr.n の値が正しく更新されるようになります。

TestRequestBodyLimit テストの変更

変更前:

	nWritten := int64(0)
	req, _ := NewRequest("POST", ts.URL, io.LimitReader(countReader{neverEnding('a'), &nWritten}, limit*200))

変更後:

	nWritten := new(int64) // ポインタとして初期化
	req, _ := NewRequest("POST", ts.URL, io.LimitReader(countReader{neverEnding('a'), nWritten}, limit*200))

変更前は nWrittenint64 型の変数として宣言され、そのアドレス &nWrittencountReader に渡されていました。これは問題ありませんが、countReadern フィールドが *int64 型であるため、new(int64) を使ってポインタとして初期化する方が、意図がより明確になります。new(int64)int64 型のゼロ値(この場合は0)で初期化された新しい int64 変数を割り当て、そのアドレスを返します。

また、nWritten の最終的な値の読み込みもアトミック操作に変更されています。

変更前:

	if nWritten > limit*100 {

変更後:

	if atomic.LoadInt64(nWritten) > limit*100 {

nWrittencountReader によって並行して更新される可能性があるため、その最終的な値を読み込む際にもデータ競合が発生する可能性があります。atomic.LoadInt64(nWritten) を使用することで、nWritten が指す値がアトミックに読み込まれ、他のGoroutineによる書き込み操作の途中の値ではなく、完全に書き込まれた最新の値が取得されることが保証されます。これにより、テストの評価が正確に行われるようになります。

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

src/pkg/net/http/serve_test.go ファイルの以下の部分が変更されました。

--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -1063,7 +1063,7 @@ type countReader struct {
 
 func (cr countReader) Read(p []byte) (n int, err error) {
 	n, err = cr.r.Read(p)
-	*cr.n += int64(n)
+	atomic.AddInt64(cr.n, int64(n))
 	return
 }
 
@@ -1081,8 +1081,8 @@ func TestRequestBodyLimit(t *testing.T) {
 	}))
 	defer ts.Close()
 
-	nWritten := int64(0)
-	req, _ := NewRequest("POST", ts.URL, io.LimitReader(countReader{neverEnding('a'), &nWritten}, limit*200))
+	nWritten := new(int64)
+	req, _ := NewRequest("POST", ts.URL, io.LimitReader(countReader{neverEnding('a'), nWritten}, limit*200))
 
 	// Send the POST, but don't care it succeeds or not.  The
 	// remote side is going to reply and then close the TCP
@@ -1095,7 +1095,7 @@ func TestRequestBodyLimit(t *testing.T) {
 	// the remote side hung up on us before we wrote too much.
 	_, _ = DefaultClient.Do(req)
 
-	if nWritten > limit*100 {
+	if atomic.LoadInt64(nWritten) > limit*100 {
 		t.Errorf("handler restricted the request body to %d bytes, but client managed to write %d",
 			limit, nWritten)
 	}

コアとなるコードの解説

countReader 構造体と Read メソッド

countReader は、内部の io.Reader (cr.r) からデータを読み込みつつ、読み込んだバイト数を cr.n が指す int64 変数に加算していくためのヘルパー構造体です。

変更前は、*cr.n += int64(n) という直接的な加算が行われていました。これは、複数のGoroutineが同時に Read メソッドを呼び出すと、cr.n の値の読み込み、加算、書き込みの各ステップがアトミックではないため、データ競合が発生する可能性がありました。例えば、cr.n が100のときに2つのGoroutineがそれぞれ10を加えようとした場合、以下のようなシーケンスで最終的な値が110になる可能性があります(期待値は120)。

  1. Goroutine A: cr.n を読み込む (100)
  2. Goroutine B: cr.n を読み込む (100)
  3. Goroutine A: 100 + 10 = 110 を計算
  4. Goroutine B: 100 + 10 = 110 を計算
  5. Goroutine A: cr.n に 110 を書き込む
  6. Goroutine B: cr.n に 110 を書き込む

変更後は、atomic.AddInt64(cr.n, int64(n)) を使用することで、この加算操作全体が単一のアトミックな操作として実行されることが保証されます。これにより、上記のようなデータ競合は発生せず、cr.n の値は常に正しく更新されるようになります。

TestRequestBodyLimit テスト

このテストは、HTTPリクエストボディのサイズ制限が正しく機能するかどうかを検証するためのものです。

  1. nWritten 変数の初期化: 変更前は nWritten := int64(0) でしたが、変更後は nWritten := new(int64) となっています。どちらも int64 型のポインタを countReader に渡すという点では同じですが、new(int64) を使うことで、nWritten がポインタであり、その指す値がアトミック操作によって更新されることがより明確になります。

  2. io.LimitReader の使用: io.LimitReader(countReader{neverEnding('a'), nWritten}, limit*200) は、neverEnding('a') (無限に 'a' を生成するリーダー) からデータを読み込み、そのバイト数を nWritten にカウントしつつ、limit*200 バイトまでしか読み込まないように制限しています。

  3. DefaultClient.Do(req): HTTPリクエストを送信します。この際、countReaderRead メソッドが呼び出され、nWritten が更新されます。

  4. nWritten の値の検証: 変更前は if nWritten > limit*100 と直接 nWritten の値を参照していましたが、nWritten は並行して更新される可能性があるため、ここでもデータ競合が発生する可能性がありました。 変更後は if atomic.LoadInt64(nWritten) > limit*100 となり、atomic.LoadInt64 を使用して nWritten の値をアトミックに読み込むことで、常に最新かつ正しい値が取得されることが保証され、テストの信頼性が向上しました。

このコミットは、Go言語における並行プログラミングのベストプラクティス、特に共有状態の変更にはアトミック操作やミューテックスなどの同期プリミティブを使用することの重要性を示しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード
  • Go言語のIssueトラッカー
  • Go言語のコードレビューシステム (Gerrit)
  • データ競合とアトミック操作に関する一般的なプログラミングの知識
  • io.Reader および io.LimitReader に関するGo言語のドキュメント
  • Go言語における並行処理の概念に関する一般的な情報源 (例: "Go Concurrency Patterns" by Rob Pike)
  • Dave Cheney氏のブログやGoに関する記事 (Goコミュニティにおける著名な貢献者の一人)