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

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

コミット

このコミット bfd3c223f944fbf1bd22fa75f96a0cd1a14066af は、Go言語の net パッケージにおけるテスト内のデータ競合を修正するものです。具体的には、Windows環境でのネットワークテスト TestAcceptIgnoreSomeErrors において発生していたデータ競合を解消しています。

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

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

元コミット内容

net: fix data race in test
Fixes #7157.

R=alex.brainman, bradfitz
CC=golang-codereviews
https://golang.org/cl/54880043

変更の背景

このコミットは、Go言語のIssue #7157 に対応するものです。Issue #7157 は、net パッケージのテスト TestAcceptIgnoreSomeErrors においてデータ競合が検出されたことを報告しています。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生する競合状態です。これはプログラムの予測不能な動作やクラッシュを引き起こす可能性があり、特に並行処理が多用されるGo言語においては、テスト段階でこのような問題を特定し修正することが重要です。

この特定のケースでは、テストコード内で err という変数が複数のゴルーチンから読み書きされる可能性があり、それがデータ競合の原因となっていました。テストの信頼性を確保し、将来的なバグの混入を防ぐために、この競合状態を解消する必要がありました。

前提知識の解説

データ競合 (Data Race)

データ競合は、並行プログラミングにおける一般的なバグの一種です。Go言語のメモリモデルでは、以下の3つの条件がすべて満たされた場合にデータ競合が発生すると定義されています。

  1. 少なくとも2つのゴルーチンが同じメモリ位置にアクセスする。
  2. 少なくとも1つのアクセスが書き込みである。
  3. それらのアクセスが同期メカニズムによって順序付けされていない。

データ競合が発生すると、プログラムの実行結果が非決定論的になり、デバッグが非常に困難になります。Go言語には、データ競合を検出するためのツールとして「Go Race Detector」が組み込まれており、go run -racego test -race のように -race フラグを付けて実行することで、実行時にデータ競合を検出できます。

Go言語の net パッケージ

net パッケージは、Go言語におけるネットワークI/Oの基本的なインターフェースを提供します。TCP/IP、UDP、Unixドメインソケットなどのネットワークプロトコルを扱うための機能が含まれています。サーバーの構築、クライアントの接続、データの送受信など、ネットワーク関連のほとんどの操作はこのパッケージを通じて行われます。

ゴルーチン (Goroutine) とチャネル (Channel)

Go言語の並行処理は、軽量なスレッドであるゴルーチンと、ゴルーチン間の安全な通信を可能にするチャネルによって実現されます。

  • ゴルーチン: go キーワードを使って関数を呼び出すことで、新しいゴルーチンが生成され、その関数が並行して実行されます。
  • チャネル: ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、データ競合を避けるための主要な同期プリミティブとして機能します。

time.Sleepalittle

time.Sleep は、指定された期間だけ現在のゴルーチンの実行を一時停止する関数です。テストコードでは、特定の操作が完了するのを待つために使用されることがありますが、これは非決定論的なテスト結果につながる可能性があるため、注意が必要です。 alittle は、このテストコード内で定義された time.Duration 型の変数で、短い時間(例えば100ミリ秒)を表すために使われています。

技術的詳細

このコミットで修正されたデータ競合は、src/pkg/net/net_windows_test.go ファイル内の TestAcceptIgnoreSomeErrors 関数で発生していました。このテストは、ネットワーク接続の確立とデータ送信に関するエラー処理をテストするものです。

問題のコードは以下の部分でした。

// Before
go func() {
    time.Sleep(alittle)
    err = send(ln.Addr().String(), "abc") // ここで外部スコープのerrに書き込み
    if err != nil {
        result <- err
    }
}()

このコードでは、新しいゴルーチンが起動され、alittle だけ待機した後、send 関数を呼び出しています。send 関数の戻り値であるエラーは、外部スコープで宣言された err 変数に代入されています。

データ競合が発生する可能性があったのは、このゴルーチンが err に書き込む一方で、メインのテストゴルーチンが err を読み取る(または別のゴルーチンが書き込む)可能性があったためです。Goのメモリモデルでは、このような非同期なアクセスが同期メカニズムなしに行われると、データ競合と見なされます。

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

変更は src/pkg/net/net_windows_test.go ファイルの1箇所のみです。

--- a/src/pkg/net/net_windows_test.go
+++ b/src/pkg/net/net_windows_test.go
@@ -107,7 +107,7 @@ func TestAcceptIgnoreSomeErrors(t *testing.T) {
 	result := make(chan error)
 	go func() {
 		time.Sleep(alittle)
-		err = send(ln.Addr().String(), "abc")
+		err := send(ln.Addr().String(), "abc")
 		if err != nil {
 			result <- err
 		}

コアとなるコードの解説

修正は非常にシンプルで効果的です。

// After
go func() {
    time.Sleep(alittle)
    err := send(ln.Addr().String(), "abc") // ここで新しいerr変数を宣言
    if err != nil {
        result <- err
    }
}()

変更点は、err = send(...) の行を err := send(...) に変更したことです。

  • 変更前 (err = send(...)): この行は、ゴルーチンの外部スコープで既に宣言されている err 変数に、send 関数の戻り値を代入していました。これにより、複数のゴルーチンが同じ err 変数に同時にアクセスし、少なくとも1つが書き込みを行うため、データ競合が発生していました。
  • 変更後 (err := send(...)): この行は、send 関数の戻り値を、このゴルーチン内の新しいローカル変数 err に代入しています。これにより、この err 変数はこのゴルーチン内でのみ有効となり、他のゴルーチンからはアクセスされなくなります。結果として、共有メモリへの非同期な書き込みがなくなり、データ競合が解消されます。

この修正は、Go言語におけるデータ競合の一般的な解決策の一つである「共有状態を避ける」という原則に従っています。各ゴルーチンが自身のローカル変数を持つことで、外部の共有変数への競合アクセスを防ぎ、並行処理の安全性を高めています。

関連リンク

参考にした情報源リンク