[インデックス 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
フィールドへのアクセスが並行処理環境下でデータ競合を引き起こす可能性が指摘されていました。
countReader
は io.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
を返します。n
は len(p)
以下である必要があります。Read
が n > 0
と err == 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))
変更前は nWritten
が int64
型の変数として宣言され、そのアドレス &nWritten
が countReader
に渡されていました。これは問題ありませんが、countReader
の n
フィールドが *int64
型であるため、new(int64)
を使ってポインタとして初期化する方が、意図がより明確になります。new(int64)
は int64
型のゼロ値(この場合は0)で初期化された新しい int64
変数を割り当て、そのアドレスを返します。
また、nWritten
の最終的な値の読み込みもアトミック操作に変更されています。
変更前:
if nWritten > limit*100 {
変更後:
if atomic.LoadInt64(nWritten) > limit*100 {
nWritten
は countReader
によって並行して更新される可能性があるため、その最終的な値を読み込む際にもデータ競合が発生する可能性があります。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)。
- Goroutine A:
cr.n
を読み込む (100) - Goroutine B:
cr.n
を読み込む (100) - Goroutine A: 100 + 10 = 110 を計算
- Goroutine B: 100 + 10 = 110 を計算
- Goroutine A:
cr.n
に 110 を書き込む - Goroutine B:
cr.n
に 110 を書き込む
変更後は、atomic.AddInt64(cr.n, int64(n))
を使用することで、この加算操作全体が単一のアトミックな操作として実行されることが保証されます。これにより、上記のようなデータ競合は発生せず、cr.n
の値は常に正しく更新されるようになります。
TestRequestBodyLimit
テスト
このテストは、HTTPリクエストボディのサイズ制限が正しく機能するかどうかを検証するためのものです。
-
nWritten
変数の初期化: 変更前はnWritten := int64(0)
でしたが、変更後はnWritten := new(int64)
となっています。どちらもint64
型のポインタをcountReader
に渡すという点では同じですが、new(int64)
を使うことで、nWritten
がポインタであり、その指す値がアトミック操作によって更新されることがより明確になります。 -
io.LimitReader
の使用:io.LimitReader(countReader{neverEnding('a'), nWritten}, limit*200)
は、neverEnding('a')
(無限に 'a' を生成するリーダー) からデータを読み込み、そのバイト数をnWritten
にカウントしつつ、limit*200
バイトまでしか読み込まないように制限しています。 -
DefaultClient.Do(req)
: HTTPリクエストを送信します。この際、countReader
のRead
メソッドが呼び出され、nWritten
が更新されます。 -
nWritten
の値の検証: 変更前はif nWritten > limit*100
と直接nWritten
の値を参照していましたが、nWritten
は並行して更新される可能性があるため、ここでもデータ競合が発生する可能性がありました。 変更後はif atomic.LoadInt64(nWritten) > limit*100
となり、atomic.LoadInt64
を使用してnWritten
の値をアトミックに読み込むことで、常に最新かつ正しい値が取得されることが保証され、テストの信頼性が向上しました。
このコミットは、Go言語における並行プログラミングのベストプラクティス、特に共有状態の変更にはアトミック操作やミューテックスなどの同期プリミティブを使用することの重要性を示しています。
関連リンク
- Go Issue #4220: https://github.com/golang/go/issues/4220
- Go CL 6638053: https://golang.org/cl/6638053
sync/atomic
パッケージのドキュメント: https://pkg.go.dev/sync/atomicio
パッケージのドキュメント: https://pkg.go.dev/io
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- Go言語のIssueトラッカー
- Go言語のコードレビューシステム (Gerrit)
- データ競合とアトミック操作に関する一般的なプログラミングの知識
io.Reader
およびio.LimitReader
に関するGo言語のドキュメント- Go言語における並行処理の概念に関する一般的な情報源 (例: "Go Concurrency Patterns" by Rob Pike)
- Dave Cheney氏のブログやGoに関する記事 (Goコミュニティにおける著名な貢献者の一人)