[インデックス 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
パッケージ、メモリモデル) - データ競合に関する一般的なプログラミングの知識