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

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

このコミットは、exp/inotify パッケージのLinuxテストにおけるデータ競合(data race)を修正するものです。具体的には、TestInotifyClose 関数内で発生していた、Close() メソッドの二重呼び出しテストにおける同期の問題を解決しています。

コミット

commit 3d2e75cf922440870596e9bc6145630b2b6a3d5d
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Jan 16 11:11:58 2012 +0400

    exp/inotify: fix data race in linux tests
    Fixes #2708.
    
    R=golang-dev, bradfitz
    CC=golang-dev, mpimenov
    https://golang.org/cl/5543060

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

https://github.com/golang/go/commit/3d2e75cf922440870596e9bc6145630b2b6a3d5d

元コミット内容

exp/inotify: fix data race in linux tests
Fixes #2708.

R=golang-dev, bradfitz
CC=golang-dev, mpimenov
https://golang.org/cl/5543060

変更の背景

この変更は、Go言語の実験的な inotify パッケージ(ファイルシステムイベントを監視するためのLinuxカーネル機能のGoラッパー)のテストコード inotify_linux_test.go において、データ競合が発生していたために行われました。

TestInotifyClose テストは、Watcher インターフェースの Close() メソッドが複数回呼び出された場合の挙動を検証することを目的としていました。具体的には、一度 Close() を呼び出した後、別のゴルーチンで再度 Close() を呼び出し、その二度目の呼び出しがブロックせずにすぐに戻ることを期待していました。

しかし、元の実装では、二度目の Close() 呼び出しが完了したかどうかを done というブール変数と time.Sleep を使ってチェックしていました。time.Sleep は指定された時間だけゴルーチンを一時停止させるため、二度目の Close() 呼び出しが time.Sleep の期間内に完了しない場合、テストが誤って失敗する可能性がありました。これは、テストの実行タイミングやシステム負荷によって結果が不安定になる、典型的なデータ競合やタイミングの問題を示唆していました。

この不安定なテストは、Fixes #2708 で言及されているIssue 2708で報告されており、その修正のためにこのコミットが作成されました。

前提知識の解説

1. inotify

inotify は、Linuxカーネルが提供するファイルシステムイベント監視メカニズムです。ファイルやディレクトリの作成、削除、移動、変更などのイベントをアプリケーションがリアルタイムで検出できるようにします。Go言語の exp/inotify パッケージは、この inotify 機能をGoプログラムから利用するためのラッパーを提供します。

2. データ競合 (Data Race)

データ競合は、並行プログラミングにおいて複数のゴルーチン(またはスレッド)が共有データに同時にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって順序付けされていない場合に発生するバグです。データ競合が発生すると、プログラムの動作が予測不能になったり、クラッシュしたりする可能性があります。Go言語では、go run -racego test -race コマンドでデータ競合を検出するツールが提供されています。

3. Go言語の並行処理と同期

Go言語は、ゴルーチン(軽量なスレッド)とチャネル(ゴルーチン間の通信と同期のためのパイプ)を用いて並行処理をサポートしています。

  • ゴルーチン (Goroutine): go キーワードを使って関数呼び出しの前に記述することで、その関数を新しいゴルーチンとして実行します。非常に軽量で、数千から数百万のゴルーチンを同時に実行できます。
  • チャネル (Channel): ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、データの受け渡しだけでなく、ゴルーチン間の同期にも使用できます。チャネルへの送信(ch <- value)と受信(value <- ch)は、デフォルトでブロックします。
  • select ステートメント: 複数のチャネル操作を待機し、準備ができた最初の操作を実行するために使用されます。タイムアウト処理や、複数のイベントソースからの入力を処理する際に非常に便利です。
  • time.After: 指定された期間が経過した後に現在時刻を送信するチャネルを返します。select ステートメントと組み合わせて、タイムアウト処理を実装する際によく使用されます。
  • time.Sleep: 指定された期間だけ現在のゴルーチンを一時停止させます。これは、厳密な同期が必要な場合には推奨されません。なぜなら、スリープ期間が短すぎるとイベントを逃す可能性があり、長すぎるとテストの実行時間が無駄に長くなるからです。

4. Goのテスト (go test)

Go言語には、標準でテストフレームワークが組み込まれています。_test.go で終わるファイルにテストコードを記述し、go test コマンドで実行します。テスト関数は Test で始まり、*testing.T 型の引数を取ります。

技術的詳細

元のコードでは、二度目の watcher.Close() 呼び出しが完了したことを確認するために、done というブール変数と time.Sleep(50 * time.Millisecond) を使用していました。

// 元のコードの関連部分
done := false
go func() {
    watcher.Close()
    done = true // ゴルーチン内で共有変数 `done` を書き込み
}()

time.Sleep(50 * time.Millisecond) // メインゴルーチンがスリープ
if !done { // メインゴルーチンが `done` を読み込み
    t.Fatal("double Close() test failed: second Close() call didn't return")
}

このコードには以下の問題がありました。

  1. データ競合: done 変数は、メインゴルーチンと新しく起動されたゴルーチンによって共有されています。新しく起動されたゴルーチンが done = true と書き込み、メインゴルーチンが if !donedone を読み込みます。これらのアクセスは同期されていません。Goのメモリモデルでは、このような非同期アクセスはデータ競合を引き起こし、done の値が期待通りにメインゴルーチンに「見える」保証はありません。
  2. タイミングの問題: time.Sleep(50 * time.Millisecond) は、二度目の Close() 呼び出しが50ミリ秒以内に完了することを「期待」しています。しかし、システム負荷やスケジューリングの状況によっては、50ミリ秒を超えても Close() が完了しない可能性があります。この場合、donetrue になる前に time.Sleep が終了し、テストが誤って失敗してしまいます。これは「フレイキーテスト(Flaky Test)」として知られる問題で、テストが非決定的な結果を返す原因となります。

このコミットでは、これらの問題を解決するために、ブール変数と time.Sleep の代わりにGoのチャネルと select ステートメントを使用しています。

// 修正後のコードの関連部分
done := make(chan bool) // チャネルを作成
go func() {
    watcher.Close()
    done <- true // チャネルに値を送信(完了を通知)
}()

select {
case <-done: // チャネルからの受信を待機
case <-time.After(50 * time.Millisecond): // タイムアウトチャネルからの受信を待機
    t.Fatal("double Close() test failed: second Close() call didn't return")
}

この修正により、以下の利点が得られます。

  1. データ競合の解消: done チャネルを介した通信は、Goのメモリモデルによって同期が保証されます。ゴルーチンが done <- true を実行すると、その書き込みはメインゴルーチンが <-done を受信したときに確実に可視化されます。これにより、データ競合が解消されます。
  2. 正確な同期: time.Sleep のような「推測」ではなく、チャネルによる明示的な同期メカニズムが導入されました。二度目の Close() 呼び出しが完了するとすぐに done <- true が実行され、メインゴルーチンは <-done でその完了を即座に検出できます。
  3. 堅牢なタイムアウト処理: select ステートメントと time.After を組み合わせることで、二度目の Close() 呼び出しが完了するか、または50ミリ秒のタイムアウトが発生するかのいずれか早い方を待つことができます。これにより、テストが不必要に長く待機することなく、かつタイムアウトによって誤って失敗することなく、正確に動作するようになります。

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

--- a/src/pkg/exp/inotify/inotify_linux_test.go
+++ b/src/pkg/exp/inotify/inotify_linux_test.go
@@ -83,14 +83,15 @@ func TestInotifyClose(t *testing.T) {
 	watcher, _ := NewWatcher()
 	watcher.Close()
 
-	done := false
+	done := make(chan bool)
 	go func() {
 		watcher.Close()
-		done = true
+		done <- true
 	}()
 
-	time.Sleep(50 * time.Millisecond)
-	if !done {
+	select {
+	case <-done:
+	case <-time.After(50 * time.Millisecond):
 		t.Fatal("double Close() test failed: second Close() call didn't return")
 	}
 

コアとなるコードの解説

変更は TestInotifyClose 関数内で行われています。

  1. done := false から done := make(chan bool) への変更:

    • 元のコードでは、done はブール型の変数で、ゴルーチン間で共有される状態として使われていました。
    • 修正後、done はブール型の値を送受信するためのチャネルになりました。チャネルは、Goにおいてゴルーチン間の安全な通信と同期のための主要なメカニズムです。make(chan bool) は、バッファなしのブール型チャネルを作成します。
  2. done = true から done <- true への変更:

    • 新しく起動されたゴルーチン内で、二度目の watcher.Close() が完了した後、元のコードでは共有変数 donetrue を直接書き込んでいました。
    • 修正後、done <- true とすることで、true の値を done チャネルに送信しています。チャネルへの送信操作は、受信側が準備できるまでブロックします。これにより、Close() の完了がメインゴルーチンに確実に通知されます。
  3. time.Sleep(50 * time.Millisecond)if !done から select ブロックへの変更:

    • 元のコードでは、メインゴルーチンは50ミリ秒間スリープし、その後 done の値をチェックしていました。これはタイミングに依存し、データ競合の可能性がありました。
    • 修正後、select ステートメントが導入されました。
      • case <-done:: これは done チャネルからの受信操作です。新しく起動されたゴルーチンが done <- true を実行すると、この case が準備完了となり、select はすぐにこのパスを実行します。これにより、Close() の完了を正確に検出できます。
      • case <-time.After(50 * time.Millisecond):: これはタイムアウト処理です。time.After は50ミリ秒後に現在時刻を送信するチャネルを返します。もし done チャネルからの受信が50ミリ秒以内に発生しなかった場合、この case が準備完了となり、select はこのパスを実行します。この場合、テストは t.Fatal を呼び出して失敗します。

この変更により、テストはより堅牢になり、タイミングの問題やデータ競合に起因する不安定な挙動が解消されました。

関連リンク

参考にした情報源リンク