[インデックス 14519] ファイルの概要
このコミットは、Go言語の実験的なWindowsファイルシステム通知パッケージ exp/winfsnotify におけるデータ競合の修正に関するものです。具体的には、TestNotifyClose というテスト関数内で発生していた、Close() メソッドの二重呼び出しテストにおける競合状態を解消しています。
コミット
commit 16a5934540864a687ed709b969ea32f1e6e8c238
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Wed Nov 28 17:01:22 2012 +1100
exp/winfsnotify: fix data race in TestNotifyClose
Fixes #4342.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6850080
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/16a5934540864a687ed709b969ea32f1e6e8c238
元コミット内容
exp/winfsnotify: fix data race in TestNotifyClose
Fixes #4342.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6850080
変更の背景
このコミットは、Go言語のIssue #4342を修正するために行われました。Issue #4342は、exp/winfsnotify パッケージの TestNotifyClose テスト関数において、Close() メソッドを二重に呼び出すシナリオでデータ競合が発生するという報告でした。
exp/winfsnotify は、Windows環境でのファイルシステムイベント(ファイルの作成、変更、削除など)を監視するための実験的なパッケージです。Watcher オブジェクトはファイルシステムイベントを監視し、Close() メソッドはその監視を停止し、関連するリソースを解放するために使用されます。
TestNotifyClose テストでは、Watcher.Close() を一度呼び出した後、別のゴルーチンで再度 Watcher.Close() を呼び出し、二度目の呼び出しがブロックされずにすぐに戻ることを期待していました。しかし、このテストの実装では、ゴルーチン間で共有される done というブール変数に対して、適切な同期メカニズムなしに読み書きが行われていました。これにより、テストが不安定になり、データ競合検出ツール(Goのgo run -raceなど)によって検出される可能性がありました。
データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生します。このような状況は予測不能な動作やバグ(例: テストの失敗、クラッシュ、不正なデータ)を引き起こす可能性があります。
前提知識の解説
1. Go言語の並行処理とゴルーチン (Goroutines)
Go言語は、軽量なスレッドである「ゴルーチン」を用いて並行処理をサポートします。ゴルーチンは go キーワードを使って関数呼び出しの前に記述することで簡単に起動でき、Goランタイムによって効率的にスケジューリングされます。
2. データ競合 (Data Race)
データ競合は、並行プログラミングにおける一般的なバグの一種です。以下の3つの条件がすべて満たされたときに発生します。
- 少なくとも2つのゴルーチンが同じメモリ位置にアクセスする。
- 少なくとも1つのアクセスが書き込みである。
- アクセスが同期メカニズムによって順序付けされていない。
データ競合が発生すると、プログラムの動作が非決定論的になり、デバッグが困難なバグにつながります。Goにはデータ競合を検出するための組み込みツール(Race Detector)があります。
3. sync/atomic パッケージ
sync/atomic パッケージは、低レベルのアトミック操作を提供します。アトミック操作とは、不可分(分割不可能)な操作のことで、他のゴルーチンから中断されることなく完全に実行されることが保証されます。これにより、ミューテックスなどのより高レベルな同期プリミティブを使用せずに、共有変数へのアクセスを安全に行うことができます。
atomic.StoreInt32(addr *int32, val int32):addrが指すint32型の変数にvalをアトミックに書き込みます。atomic.LoadInt32(addr *int32) int32:addrが指すint32型の変数の値をアトミックに読み込みます。
これらの関数は、CPUのハードウェア命令レベルでアトミック性を保証するため、非常に高速です。ブール値のフラグを安全に設定・読み込みたい場合など、単純な共有変数の操作に適しています。
4. exp/winfsnotify パッケージ
exp/winfsnotify は、Go言語の標準ライブラリの一部としてではなく、golang.org/x/exp リポジトリで提供されていた実験的なパッケージです。これは、Windowsオペレーティングシステムが提供するファイルシステム変更通知API(例: ReadDirectoryChangesW)を利用して、特定のディレクトリやファイルの変更を監視する機能を提供します。このパッケージは、後に標準ライブラリの fsnotify パッケージ(クロスプラットフォーム対応)に統合されるか、その開発の基礎となりました。
技術的詳細
このコミットの技術的詳細は、データ競合の典型的な修正パターンを示しています。元のコードでは、done というブール変数が、メインゴルーチンと、Close() を二重に呼び出すための匿名ゴルーチンの間で共有されていました。
// 変更前
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) が、異なるゴルーチンから同期なしに行われています。Goのメモリモデルでは、このような非同期アクセスはデータ競合を引き起こす可能性があります。コンパイラやCPUは、最適化のためにこれらの操作の順序を変更したり、キャッシュされた値を読み込んだりする可能性があり、done の更新がメインゴルーチンに「見えない」状態になることがあります。
修正では、この done 変数を int32 型に変更し、sync/atomic パッケージのアトミック操作を使用することで、この問題を解決しています。
// 変更後
var done int32 // int32型に変更
go func() {
watcher.Close()
atomic.StoreInt32(&done, 1) // アトミックに1を書き込み
}()
time.Sleep(50 * time.Millisecond)
if atomic.LoadInt32(&done) == 0 { // アトミックに読み込み
t.Fatal("double Close() test failed: second Close() call didn't return")
}
atomic.StoreInt32 と atomic.LoadInt32 は、メモリバリア(memory barrier)を暗黙的に提供します。これにより、StoreInt32 による書き込みが LoadInt32 による読み込みよりも前に完了することが保証され、done 変数の値がゴルーチン間で正しく伝播されるようになります。int32 を使用しているのは、sync/atomic パッケージが提供するアトミック操作が、int32, int64, uint32, uint64, Pointer などの特定のプリミティブ型に対して定義されているためです。ブール値の true/false は、それぞれ 1/0 にマッピングして int32 で表現するのが一般的です。
この修正により、TestNotifyClose はデータ競合なしに安定して動作するようになり、Close() の二重呼び出しが期待通りに機能するかどうかを正確にテストできるようになりました。
コアとなるコードの変更箇所
変更は src/pkg/exp/winfsnotify/winfsnotify_test.go ファイルの TestNotifyClose 関数内で行われています。
--- a/src/pkg/exp/winfsnotify/winfsnotify_test.go
+++ b/src/pkg/exp/winfsnotify/winfsnotify_test.go
@@ -9,6 +9,7 @@ package winfsnotify
import (
"io/ioutil"
"os"
+ "sync/atomic" // sync/atomic パッケージのインポートを追加
"testing"
"time"
)
@@ -105,14 +106,14 @@ func TestNotifyClose(t *testing.T) {
watcher, _ := NewWatcher()
watcher.Close()
- done := false // ブール変数から
- go func() {
- watcher.Close()
- done = true // 直接書き込み
- }()
-
- time.Sleep(50 * time.Millisecond)
- if !done { // 直接読み込み
+ var done int32 // int32型のアトミック変数に変更
+ go func() {
+ watcher.Close()
+ atomic.StoreInt32(&done, 1) // アトミックな書き込み
+ }()
+
+ time.Sleep(50 * time.Millisecond)
+ if atomic.LoadInt32(&done) == 0 { // アトミックな読み込み
t.Fatal("double Close() test failed: second Close() call didn't return")
}
コアとなるコードの解説
-
import "sync/atomic"の追加:sync/atomicパッケージの関数を使用するために、ファイルの冒頭にインポート文が追加されました。 -
done := falseからvar done int32への変更:done変数の型がboolからint32に変更されました。これは、sync/atomicパッケージがint32型に対するアトミック操作を提供するためです。初期値はint32のゼロ値である0となります。 -
done = trueからatomic.StoreInt32(&done, 1)への変更: 匿名ゴルーチン内でdone変数に値を設定する際に、直接trueを代入する代わりに、atomic.StoreInt32関数が使用されました。これにより、done変数への書き込みがアトミックに行われることが保証されます。1はtrueを意味します。 -
if !doneからif atomic.LoadInt32(&done) == 0への変更: メインゴルーチンでdone変数の値をチェックする際に、直接!doneを評価する代わりに、atomic.LoadInt32関数が使用されました。これにより、done変数からの読み込みがアトミックに行われることが保証されます。0はfalseを意味します。
これらの変更により、done 変数へのアクセスがすべてアトミック操作によって行われるようになり、複数のゴルーチンからの同時アクセスによるデータ競合が解消されました。これにより、テストの信頼性が向上し、Goのメモリモデルに準拠した安全な並行処理が実現されています。
関連リンク
- Go Issue #4342: https://github.com/golang/go/issues/4342
- Go CL 6850080: https://golang.org/cl/6850080 (Goのコードレビューシステム Gerrit のリンク)
- Go言語の
sync/atomicパッケージのドキュメント: https://pkg.go.dev/sync/atomic - Go言語のメモリモデル: https://go.dev/ref/mem
参考にした情報源リンク
- Go Issue #4342の議論内容
- Go CL 6850080の変更内容とレビューコメント
- Go言語の公式ドキュメント(
sync/atomicパッケージ、メモリモデル) - データ競合に関する一般的なプログラミングの知識```markdown
[インデックス 14519] ファイルの概要
このコミットは、Go言語の実験的なWindowsファイルシステム通知パッケージ exp/winfsnotify におけるデータ競合の修正に関するものです。具体的には、TestNotifyClose というテスト関数内で発生していた、Close() メソッドの二重呼び出しテストにおける競合状態を解消しています。
コミット
commit 16a5934540864a687ed709b969ea32f1e6e8c238
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Wed Nov 28 17:01:22 2012 +1100
exp/winfsnotify: fix data race in TestNotifyClose
Fixes #4342.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6850080
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/16a5934540864a687ed709b969ea32f1e6e8c238
元コミット内容
exp/winfsnotify: fix data race in TestNotifyClose
Fixes #4342.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6850080
変更の背景
このコミットは、Go言語のIssue #4342を修正するために行われました。Issue #4342は、exp/winfsnotify パッケージの TestNotifyClose テスト関数において、Close() メソッドを二重に呼び出すシナリオでデータ競合が発生するという報告でした。
exp/winfsnotify は、Windows環境でのファイルシステムイベント(ファイルの作成、変更、削除など)を監視するための実験的なパッケージです。Watcher オブジェクトはファイルシステムイベントを監視し、Close() メソッドはその監視を停止し、関連するリソースを解放するために使用されます。
TestNotifyClose テストでは、Watcher.Close() を一度呼び出した後、別のゴルーチンで再度 Watcher.Close() を呼び出し、二度目の呼び出しがブロックされずにすぐに戻ることを期待していました。しかし、このテストの実装では、ゴルーチン間で共有される done というブール変数に対して、適切な同期メカニズムなしに読み書きが行われていました。これにより、テストが不安定になり、データ競合検出ツール(Goのgo run -raceなど)によって検出される可能性がありました。
データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生します。このような状況は予測不能な動作やバグ(例: テストの失敗、クラッシュ、不正なデータ)を引き起こす可能性があります。
前提知識の解説
1. Go言語の並行処理とゴルーチン (Goroutines)
Go言語は、軽量なスレッドである「ゴルーチン」を用いて並行処理をサポートします。ゴルーチンは go キーワードを使って関数呼び出しの前に記述することで簡単に起動でき、Goランタイムによって効率的にスケジューリングされます。
2. データ競合 (Data Race)
データ競合は、並行プログラミングにおける一般的なバグの一種です。以下の3つの条件がすべて満たされたときに発生します。
- 少なくとも2つのゴルーチンが同じメモリ位置にアクセスする。
- 少なくとも1つのアクセスが書き込みである。
- アクセスが同期メカニズムによって順序付けされていない。
データ競合が発生すると、プログラムの動作が非決定論的になり、デバッグが困難なバグにつながります。Goにはデータ競合を検出するための組み込みツール(Race Detector)があります。
3. sync/atomic パッケージ
sync/atomic パッケージは、低レベルのアトミック操作を提供します。アトミック操作とは、不可分(分割不可能)な操作のことで、他のゴルーチンから中断されることなく完全に実行されることが保証されます。これにより、ミューテックスなどのより高レベルな同期プリミティブを使用せずに、共有変数へのアクセスを安全に行うことができます。
atomic.StoreInt32(addr *int32, val int32):addrが指すint32型の変数にvalをアトミックに書き込みます。atomic.LoadInt32(addr *int32) int32:addrが指すint32型の変数の値をアトミックに読み込みます。
これらの関数は、CPUのハードウェア命令レベルでアトミック性を保証するため、非常に高速です。ブール値のフラグを安全に設定・読み込みたい場合など、単純な共有変数の操作に適しています。
4. exp/winfsnotify パッケージ
exp/winfsnotify は、Go言語の標準ライブラリの一部としてではなく、golang.org/x/exp リポジトリで提供されていた実験的なパッケージです。これは、Windowsオペレーティングシステムが提供するファイルシステム変更通知API(例: ReadDirectoryChangesW)を利用して、特定のディレクトリやファイルの変更を監視する機能を提供します。このパッケージは、後に標準ライブラリの fsnotify パッケージ(クロスプラットフォーム対応)に統合されるか、その開発の基礎となりました。
技術的詳細
このコミットの技術的詳細は、データ競合の典型的な修正パターンを示しています。元のコードでは、done というブール変数が、メインゴルーチンと、Close() を二重に呼び出すための匿名ゴルーチンの間で共有されていました。
// 変更前
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) が、異なるゴルーチンから同期なしに行われています。Goのメモリモデルでは、このような非同期アクセスはデータ競合を引き起こす可能性があります。コンパイラやCPUは、最適化のためにこれらの操作の順序を変更したり、キャッシュされた値を読み込んだりする可能性があり、done の更新がメインゴルーチンに「見えない」状態になることがあります。
修正では、この done 変数を int32 型に変更し、sync/atomic パッケージのアトミック操作を使用することで、この問題を解決しています。
// 変更後
var done int32 // int32型に変更
go func() {
watcher.Close()
atomic.StoreInt32(&done, 1) // アトミックに1を書き込み
}()
time.Sleep(50 * time.Millisecond)
if atomic.LoadInt32(&done) == 0 { // アトミックに読み込み
t.Fatal("double Close() test failed: second Close() call didn't return")
}
atomic.StoreInt32 と atomic.LoadInt32 は、メモリバリア(memory barrier)を暗黙的に提供します。これにより、StoreInt32 による書き込みが LoadInt32 による読み込みよりも前に完了することが保証され、done 変数の値がゴルーチン間で正しく伝播されるようになります。int32 を使用しているのは、sync/atomic パッケージが提供するアトミック操作が、int32, int64, uint32, uint64, Pointer などの特定のプリミティブ型に対して定義されているためです。ブール値の true/false は、それぞれ 1/0 にマッピングして int32 で表現するのが一般的です。
この修正により、TestNotifyClose はデータ競合なしに安定して動作するようになり、Close() の二重呼び出しが期待通りに機能するかどうかを正確にテストできるようになりました。
コアとなるコードの変更箇所
変更は src/pkg/exp/winfsnotify/winfsnotify_test.go ファイルの TestNotifyClose 関数内で行われています。
--- a/src/pkg/exp/winfsnotify/winfsnotify_test.go
+++ b/src/pkg/exp/winfsnotify/winfsnotify_test.go
@@ -9,6 +9,7 @@ package winfsnotify
import (
"io/ioutil"
"os"
+ "sync/atomic" // sync/atomic パッケージのインポートを追加
"testing"
"time"
)
@@ -105,14 +106,14 @@ func TestNotifyClose(t *testing.T) {
watcher, _ := NewWatcher()
watcher.Close()
- done := false // ブール変数から
- go func() {
- watcher.Close()
- done = true // 直接書き込み
- }()
-
- time.Sleep(50 * time.Millisecond)
- if !done { // 直接読み込み
+ var done int32 // int32型のアトミック変数に変更
+ go func() {
+ watcher.Close()
+ atomic.StoreInt32(&done, 1) // アトミックな書き込み
+ }()
+
+ time.Sleep(50 * time.Millisecond)
+ if atomic.LoadInt32(&done) == 0 { // アトミックな読み込み
t.Fatal("double Close() test failed: second Close() call didn't return")
}
コアとなるコードの解説
-
import "sync/atomic"の追加:sync/atomicパッケージの関数を使用するために、ファイルの冒頭にインポート文が追加されました。 -
done := falseからvar done int32への変更:done変数の型がboolからint32に変更されました。これは、sync/atomicパッケージがint32型に対するアトミック操作を提供するためです。初期値はint32のゼロ値である0となります。 -
done = trueからatomic.StoreInt32(&done, 1)への変更: 匿名ゴルーチン内でdone変数に値を設定する際に、直接trueを代入する代わりに、atomic.StoreInt32関数が使用されました。これにより、done変数への書き込みがアトミックに行われることが保証されます。1はtrueを意味します。 -
if !doneからif atomic.LoadInt32(&done) == 0への変更: メインゴルーチンでdone変数の値をチェックする際に、直接!doneを評価する代わりに、atomic.LoadInt32関数が使用されました。これにより、done変数からの読み込みがアトミックに行われることが保証されます。0はfalseを意味します。
これらの変更により、done 変数へのアクセスがすべてアトミック操作によって行われるようになり、複数のゴルーチンからの同時アクセスによるデータ競合が解消されました。これにより、テストの信頼性が向上し、Goのメモリモデルに準拠した安全な並行処理が実現されています。
関連リンク
- Go Issue #4342: https://github.com/golang/go/issues/4342
- Go CL 6850080: https://golang.org/cl/6850080 (Goのコードレビューシステム Gerrit のリンク)
- Go言語の
sync/atomicパッケージのドキュメント: https://pkg.go.dev/sync/atomic - Go言語のメモリモデル: https://go.dev/ref/mem
参考にした情報源リンク
- Go Issue #4342の議論内容
- Go CL 6850080の変更内容とレビューコメント
- Go言語の公式ドキュメント(
sync/atomicパッケージ、メモリモデル) - データ競合に関する一般的なプログラミングの知識