[インデックス 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 -race
や go 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")
}
このコードには以下の問題がありました。
- データ競合:
done
変数は、メインゴルーチンと新しく起動されたゴルーチンによって共有されています。新しく起動されたゴルーチンがdone = true
と書き込み、メインゴルーチンがif !done
でdone
を読み込みます。これらのアクセスは同期されていません。Goのメモリモデルでは、このような非同期アクセスはデータ競合を引き起こし、done
の値が期待通りにメインゴルーチンに「見える」保証はありません。 - タイミングの問題:
time.Sleep(50 * time.Millisecond)
は、二度目のClose()
呼び出しが50ミリ秒以内に完了することを「期待」しています。しかし、システム負荷やスケジューリングの状況によっては、50ミリ秒を超えてもClose()
が完了しない可能性があります。この場合、done
がtrue
になる前に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")
}
この修正により、以下の利点が得られます。
- データ競合の解消:
done
チャネルを介した通信は、Goのメモリモデルによって同期が保証されます。ゴルーチンがdone <- true
を実行すると、その書き込みはメインゴルーチンが<-done
を受信したときに確実に可視化されます。これにより、データ競合が解消されます。 - 正確な同期:
time.Sleep
のような「推測」ではなく、チャネルによる明示的な同期メカニズムが導入されました。二度目のClose()
呼び出しが完了するとすぐにdone <- true
が実行され、メインゴルーチンは<-done
でその完了を即座に検出できます。 - 堅牢なタイムアウト処理:
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
関数内で行われています。
-
done := false
からdone := make(chan bool)
への変更:- 元のコードでは、
done
はブール型の変数で、ゴルーチン間で共有される状態として使われていました。 - 修正後、
done
はブール型の値を送受信するためのチャネルになりました。チャネルは、Goにおいてゴルーチン間の安全な通信と同期のための主要なメカニズムです。make(chan bool)
は、バッファなしのブール型チャネルを作成します。
- 元のコードでは、
-
done = true
からdone <- true
への変更:- 新しく起動されたゴルーチン内で、二度目の
watcher.Close()
が完了した後、元のコードでは共有変数done
にtrue
を直接書き込んでいました。 - 修正後、
done <- true
とすることで、true
の値をdone
チャネルに送信しています。チャネルへの送信操作は、受信側が準備できるまでブロックします。これにより、Close()
の完了がメインゴルーチンに確実に通知されます。
- 新しく起動されたゴルーチン内で、二度目の
-
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
を呼び出して失敗します。
- 元のコードでは、メインゴルーチンは50ミリ秒間スリープし、その後
この変更により、テストはより堅牢になり、タイミングの問題やデータ競合に起因する不安定な挙動が解消されました。
関連リンク
- Go Issue 2708: https://github.com/golang/go/issues/2708
- Go CL 5543060: https://golang.org/cl/5543060
参考にした情報源リンク
- Go言語公式ドキュメント: https://go.dev/
- Go言語の並行処理に関するドキュメント (A Tour of Go - Concurrency): https://go.dev/tour/concurrency/1
- Go言語のメモリモデル: https://go.dev/ref/mem
inotify
man page (Linux):man inotify
(これはWeb検索で得られる情報源ではありませんが、inotify
の詳細な情報源です)- Goのテストに関するドキュメント: https://go.dev/pkg/testing/