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

[インデックス 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プリミティブを提供します。このパッケージの関数は、ネットワーク通信中にブロック(処理が一時停止)することがよくあります。例えば、ReadWrite操作は、データが利用可能になるまで、またはデータが送信されるまでブロックする可能性があります。

4. レースコンディション (Race Condition)

前述の通り、複数の並行プロセスやスレッドが共有リソースにアクセスし、そのアクセス順序によってプログラムの最終結果が非決定的に変わる状況を指します。このコミットでは、複数のgoroutineが同じerr変数に同時に書き込もうとすることで発生していました。

5. Goのレース検出器 (Race Detector)

Goには、プログラム実行中にレースコンディションを検出するための組み込みツールである「レース検出器」があります。これは、go run -racego build -racego 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によって共有されていた点にあります。

  1. 読み取りgoroutine: c.Read(buf[:])の結果を共有のerr変数に代入します。
  2. 書き込み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行ですが、その影響は大きいです。

  1. _, err = c.Read(buf[:]) から _, err := c.Read(buf[:])

    • 元のコードでは、c.Read(buf[:])の戻り値であるエラーを、外側のスコープ(TestReadWriteDeadline関数)で宣言されたerr変数に代入していました。
    • 修正後では、:=を使用することで、このgoroutineのローカルスコープで新しいerr変数を宣言し、初期化しています。これにより、このerr変数は他のgoroutineから独立したものとなります。
  2. _, err = c.Write(buf[:]) から _, err := c.Write(buf[:])

    • 同様に、元のコードではc.Write(buf[:])のエラーを外側の共有err変数に代入していました。
    • 修正後では、:=を使用することで、この書き込みgoroutineのローカルスコープで新しいerr変数を宣言し、初期化しています。

この変更により、各goroutineは自身のエラー処理結果を格納するための独立した変数を持つことになり、複数のgoroutineが同時に同じerr変数に書き込もうとする競合状態が完全に解消されました。これは、Go言語における並行処理のベストプラクティスの一つである「共有状態を避ける」という原則に則った修正と言えます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (変数宣言、並行処理、netパッケージなど)
  • Go Race Detectorに関する情報 (Goの公式ブログやドキュメント)