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

[インデックス 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.StoreInt32atomic.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")
 	}

コアとなるコードの解説

  1. import "sync/atomic" の追加: sync/atomic パッケージの関数を使用するために、ファイルの冒頭にインポート文が追加されました。

  2. done := false から var done int32 への変更: done 変数の型が bool から int32 に変更されました。これは、sync/atomic パッケージが int32 型に対するアトミック操作を提供するためです。初期値は int32 のゼロ値である 0 となります。

  3. done = true から atomic.StoreInt32(&done, 1) への変更: 匿名ゴルーチン内で done 変数に値を設定する際に、直接 true を代入する代わりに、atomic.StoreInt32 関数が使用されました。これにより、done 変数への書き込みがアトミックに行われることが保証されます。1true を意味します。

  4. if !done から if atomic.LoadInt32(&done) == 0 への変更: メインゴルーチンで done 変数の値をチェックする際に、直接 !done を評価する代わりに、atomic.LoadInt32 関数が使用されました。これにより、done 変数からの読み込みがアトミックに行われることが保証されます。0false を意味します。

これらの変更により、done 変数へのアクセスがすべてアトミック操作によって行われるようになり、複数のゴルーチンからの同時アクセスによるデータ競合が解消されました。これにより、テストの信頼性が向上し、Goのメモリモデルに準拠した安全な並行処理が実現されています。

関連リンク

参考にした情報源リンク

  • 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.StoreInt32atomic.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")
 	}

コアとなるコードの解説

  1. import "sync/atomic" の追加: sync/atomic パッケージの関数を使用するために、ファイルの冒頭にインポート文が追加されました。

  2. done := false から var done int32 への変更: done 変数の型が bool から int32 に変更されました。これは、sync/atomic パッケージが int32 型に対するアトミック操作を提供するためです。初期値は int32 のゼロ値である 0 となります。

  3. done = true から atomic.StoreInt32(&done, 1) への変更: 匿名ゴルーチン内で done 変数に値を設定する際に、直接 true を代入する代わりに、atomic.StoreInt32 関数が使用されました。これにより、done 変数への書き込みがアトミックに行われることが保証されます。1true を意味します。

  4. if !done から if atomic.LoadInt32(&done) == 0 への変更: メインゴルーチンで done 変数の値をチェックする際に、直接 !done を評価する代わりに、atomic.LoadInt32 関数が使用されました。これにより、done 変数からの読み込みがアトミックに行われることが保証されます。0false を意味します。

これらの変更により、done 変数へのアクセスがすべてアトミック操作によって行われるようになり、複数のゴルーチンからの同時アクセスによるデータ競合が解消されました。これにより、テストの信頼性が向上し、Goのメモリモデルに準拠した安全な並行処理が実現されています。

関連リンク

参考にした情報源リンク

  • Go Issue #4342の議論内容
  • Go CL 6850080の変更内容とレビューコメント
  • Go言語の公式ドキュメント(sync/atomic パッケージ、メモリモデル)
  • データ競合に関する一般的なプログラミングの知識