[インデックス 14294] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet
パッケージ内のTestReadWriteDeadline
テストにおけるレースコンディションを修正するものです。具体的には、テスト内でgoroutine間で共有されていたerr
変数の扱いを修正し、各goroutineが自身のローカルなエラー変数を持つように変更することで、競合状態を解消しています。この問題は、Goのレース検出器にOBLOCK
サポートが追加されたことで発見されました。
コミット
- コミットハッシュ:
824b332652b94fea18d1e6cc42e75870b0185f5e
- 作者: Rémy Oudompheng oudomphe@phare.normalesup.org
- 日付: 2012年11月1日 木曜日 20:52:30 +0100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/824b332652b94fea18d1e6cc42e75870b0185f5e
元コミット内容
net: fix race in TestReadWriteDeadline.
Discovered by adding OBLOCK support to race
instrumentation.
R=golang-dev, minux.ma, rsc
CC=golang-dev
https://golang.org/cl/6819067
変更の背景
この変更は、Go言語の標準ライブラリnet
パッケージのテストコードsrc/pkg/net/timeout_test.go
に存在するレースコンディション(競合状態)を修正するために行われました。
レースコンディションは、複数の並行に動作する処理(この場合はgoroutine)が共有リソース(この場合はerr
変数)に同時にアクセスし、そのアクセス順序によって結果が非決定的に変わってしまうバグの一種です。このようなバグは再現が難しく、発見が困難な場合が多いです。
コミットメッセージによると、このレースコンディションは「OBLOCK
サポートをレース計測に追加したことで発見された」とあります。これは、Goのレース検出器(Race Detector)の機能が強化され、より高度な競合状態を検出できるようになった結果、これまで見過ごされていたバグが顕在化したことを示しています。OBLOCK
は、GoのランタイムがI/O操作などでブロックされる際に、そのブロックがレース検出器によって適切に監視されるようにするための内部的なメカニズムであると考えられます。これにより、ネットワークI/Oに関連するテストで発生する可能性のある、より複雑なレースコンディションも検出可能になったと推測されます。
前提知識の解説
1. Go言語の変数宣言と代入 (=
と :=
)
Go言語には、変数を宣言し値を代入する方法が主に2つあります。
-
var
キーワードと=
:var err error // errをerror型として宣言 err = someFunction() // errに値を代入
これは一般的な変数宣言と代入の方法です。
err
変数は、その変数が宣言されたスコープ全体で利用可能です。 -
短い変数宣言 (
:=
):err := someFunction() // errを宣言し、someFunction()の戻り値で初期化
これはGo言語特有の便利な構文で、変数の宣言と初期化を同時に行います。
:=
が使われた場合、その変数は新しいローカル変数として宣言され、そのステートメントが存在するブロック({}
で囲まれた範囲)内でのみ有効なスコープを持ちます。もし同じ名前の変数が既に外側のスコープで存在していても、:=
を使うと内側のスコープで新しい変数が「シャドーイング(隠蔽)」されます。
このコミットの修正は、この=
と:=
の挙動の違いがレースコンディションを引き起こしていたことに起因します。
2. Go言語の並行処理とGoroutine
Go言語は、軽量なスレッドである「goroutine」と、それらの間で安全にデータをやり取りするための「チャネル」というプリミティブを提供することで、並行処理を強力にサポートしています。
- Goroutine:
go
キーワードを関数の呼び出しの前につけることで、その関数を新しいgoroutineとして実行します。goroutineはOSのスレッドよりもはるかに軽量で、数千、数万のgoroutineを同時に実行することが可能です。 - 並行処理における共有状態: 複数のgoroutineが同じメモリ上の変数(共有状態)に同時にアクセスし、少なくとも1つが書き込みを行う場合、レースコンディションが発生する可能性があります。Goでは、このような問題を避けるために「共有メモリを通信するのではなく、通信によってメモリを共有する」という哲学が推奨されています。
3. Goのnet
パッケージとネットワークI/O
net
パッケージは、TCP/IP、UDP、UnixドメインソケットなどのネットワークI/Oプリミティブを提供します。このパッケージの関数は、ネットワーク通信中にブロック(処理が一時停止)することがよくあります。例えば、Read
やWrite
操作は、データが利用可能になるまで、またはデータが送信されるまでブロックする可能性があります。
4. レースコンディション (Race Condition)
前述の通り、複数の並行プロセスやスレッドが共有リソースにアクセスし、そのアクセス順序によってプログラムの最終結果が非決定的に変わる状況を指します。このコミットでは、複数のgoroutineが同じerr
変数に同時に書き込もうとすることで発生していました。
5. Goのレース検出器 (Race Detector)
Goには、プログラム実行中にレースコンディションを検出するための組み込みツールである「レース検出器」があります。これは、go run -race
、go build -race
、go test -race
などのコマンドで有効にできます。レース検出器は、共有メモリへのアクセスを監視し、競合するアクセスパターンを特定することで、開発者がレースコンディションを特定し修正するのを助けます。
このコミットで言及されている「OBLOCK
サポート」は、レース検出器がI/Oブロック操作中にも競合状態をより正確に検出できるようにするための、検出器の内部的な改善を指しています。
技術的詳細
TestReadWriteDeadline
テストは、ネットワーク接続(net.Conn
)に対する読み書き操作が、設定されたデッドライン(タイムアウト)によって適切に中断されることを検証するためのものです。このテストでは、複数のgoroutineが作成され、それぞれが接続に対して読み取りまたは書き込みを試みます。
元のコードでは、テスト関数TestReadWriteDeadline
のスコープでerr
というerror
型の変数が宣言されていました。
func TestReadWriteDeadline(t *testing.T) {
// ...
var err error // ここでerrが宣言されている
// ...
go func() {
var buf [10]byte
_, err = c.Read(buf[:]) // 1つ目のgoroutineが外側のerr変数に代入
// ...
}()
go func() {
var buf [10000]byte
for {
_, err = c.Write(buf[:]) // 2つ目のgoroutineが外側のerr変数に代入
// ...
}
}()
// ...
}
問題は、TestReadWriteDeadline
関数内で宣言された単一のerr
変数が、その内部で起動される複数のgoroutineによって共有されていた点にあります。
- 読み取りgoroutine:
c.Read(buf[:])
の結果を共有のerr
変数に代入します。 - 書き込みgoroutine:
c.Write(buf[:])
の結果を共有のerr
変数に代入します。
これらのgoroutineは並行して実行されるため、どちらのgoroutineが先にerr
変数に書き込むか、あるいは同時に書き込もうとするかによって、err
変数の最終的な値が非決定的に変わる可能性があります。特に、レース検出器がOBLOCK
サポートによってI/Oブロック中の競合を検出できるようになったことで、この共有変数への同時書き込みがレースコンディションとして報告されるようになりました。
修正は、この共有されていたerr
変数を、各goroutine内でローカルな新しい変数として宣言し直すことで行われました。具体的には、=
による代入を:=
による短い変数宣言に変更しています。
// 修正前: _, err = c.Read(buf[:])
// 修正後: _, err := c.Read(buf[:]) // 新しいローカル変数errを宣言
// 修正前: _, err = c.Write(buf[:])
// 修正後: _, err := c.Write(buf[:]) // 新しいローカル変数errを宣言
これにより、各goroutineはそれぞれ独立したerr
変数を持つことになり、互いに干渉することがなくなります。結果として、共有状態への競合アクセスがなくなり、レースコンディションが解消されました。
コアとなるコードの変更箇所
src/pkg/net/timeout_test.go
ファイルが変更されました。
diff --git a/src/pkg/net/timeout_test.go b/src/pkg/net/timeout_test.go
index 3343c4a551..f6f92409df 100644
--- a/src/pkg/net/timeout_test.go
+++ b/src/pkg/net/timeout_test.go
@@ -201,7 +201,7 @@ func TestReadWriteDeadline(t *testing.T) {
go func() {
var buf [10]byte
- _, err = c.Read(buf[:])
+ _, err := c.Read(buf[:])
if err == nil {
t.Errorf("Read should not succeed")
}
@@ -212,7 +212,7 @@ func TestReadWriteDeadline(t *testing.T) {
go func() {
var buf [10000]byte
for {
- _, err = c.Write(buf[:])
+ _, err := c.Write(buf[:])
if err != nil {
break
}
コアとなるコードの解説
変更はわずか2行ですが、その影響は大きいです。
-
_, err = c.Read(buf[:])
から_, err := c.Read(buf[:])
へ- 元のコードでは、
c.Read(buf[:])
の戻り値であるエラーを、外側のスコープ(TestReadWriteDeadline
関数)で宣言されたerr
変数に代入していました。 - 修正後では、
:=
を使用することで、このgoroutineのローカルスコープで新しいerr
変数を宣言し、初期化しています。これにより、このerr
変数は他のgoroutineから独立したものとなります。
- 元のコードでは、
-
_, err = c.Write(buf[:])
から_, err := c.Write(buf[:])
へ- 同様に、元のコードでは
c.Write(buf[:])
のエラーを外側の共有err
変数に代入していました。 - 修正後では、
:=
を使用することで、この書き込みgoroutineのローカルスコープで新しいerr
変数を宣言し、初期化しています。
- 同様に、元のコードでは
この変更により、各goroutineは自身のエラー処理結果を格納するための独立した変数を持つことになり、複数のgoroutineが同時に同じerr
変数に書き込もうとする競合状態が完全に解消されました。これは、Go言語における並行処理のベストプラクティスの一つである「共有状態を避ける」という原則に則った修正と言えます。
関連リンク
- Go CL (Code Review) リンク: https://golang.org/cl/6819067
参考にした情報源リンク
- Go言語の公式ドキュメント (変数宣言、並行処理、netパッケージなど)
- Go Race Detectorに関する情報 (Goの公式ブログやドキュメント)