[インデックス 13378] ファイルの概要
このコミットは、Go言語の実験的なinotify
パッケージにおけるテスト中のデータ競合(data race)を防止するための修正です。具体的には、inotify_linux_test.go
ファイル内でイベント受信数をカウントする際に発生していた競合状態を、sync/atomic
パッケージの関数を使用することで安全に修正しています。
コミット
commit f5f3c3fe093fc359045a3818d3cd04f7b40b06c2
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date: Sun Jun 24 19:22:48 2012 -0400
exp/inotify: prevent data race during testing
Fixes #3714.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6341047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f5f3c3fe093fc359045a3818d3cd04f7b40b06c2
元コミット内容
exp/inotify: prevent data race during testing
Fixes #3714.
変更の背景
このコミットの背景には、Go言語のexp/inotify
パッケージのテストコードにおいて、並行処理中に発生するデータ競合の問題がありました。inotify
はLinuxカーネルのファイルシステムイベント監視機能を提供するもので、このパッケージはそのGo言語バインディングです。テストコードでは、ファイルシステムイベントを監視し、発生したイベントの数をカウントしていました。
問題は、イベントを非同期で受信するゴルーチン(goroutine)と、そのイベント数を参照するメインのテストゴルーチンとの間で、共有変数eventsReceived
へのアクセスが同期されていなかった点にあります。複数のゴルーチンが同時に同じ変数に対して読み書きを行うと、予期せぬ結果やテストの不安定性(テストが時々失敗する、または誤った結果を報告する)を引き起こすデータ競合が発生します。
コミットメッセージにあるFixes #3714.
は、このデータ競合がGoのIssueトラッカーで報告された問題(Issue 3714)を修正するものであることを示しています。データ競合はデバッグが困難なバグの一つであり、テストの信頼性を確保するためには、このような並行処理の問題を適切に解決する必要があります。
前提知識の解説
1. データ競合 (Data Race)
データ競合は、複数の並行実行されるスレッド(Goではゴルーチン)が、少なくとも1つが書き込み操作である共有メモリ上の同じ変数に同時にアクセスする際に発生する競合状態の一種です。データ競合が発生すると、プログラムの動作が非決定論的になり、予期せぬ結果、クラッシュ、またはセキュリティ脆弱性につながる可能性があります。Go言語では、データ競合を検出するためのツール(go run -race
)が提供されています。
2. sync/atomic
パッケージ
Go言語の標準ライブラリであるsync/atomic
パッケージは、低レベルのアトミック操作を提供します。アトミック操作とは、不可分(分割不可能)な操作のことで、その操作が完了するまで他のゴルーチンから割り込まれることがありません。これにより、複数のゴルーチンが共有変数にアクセスする際にデータ競合を防ぎ、安全な並行処理を実現できます。
主なアトミック操作には以下のようなものがあります。
AddInt32
,AddInt64
: 整数値の加算をアトミックに行う。LoadInt32
,LoadInt64
,LoadPointer
など: 変数の値をアトミックに読み込む。StoreInt32
,StoreInt64
,StorePointer
など: 変数に値をアトミックに書き込む。CompareAndSwapInt32
,CompareAndSwapInt64
など: 特定の値であれば新しい値に交換する(CAS操作)。
これらの関数は、ミューテックス(sync.Mutex
)のようなロック機構よりも粒度が細かく、特定の単純な操作においてはより効率的です。
3. inotify
(Linux)
inotify
はLinuxカーネルが提供するファイルシステムイベント監視メカニズムです。ファイルやディレクトリに対する変更(作成、削除、移動、アクセス、変更など)を監視し、イベントが発生した際にアプリケーションに通知します。Goのexp/inotify
パッケージは、このinotify
機能へのGo言語からのインターフェースを提供します。
4. Goの並行処理 (Goroutines and Channels)
Go言語は、軽量なスレッドである「ゴルーチン(goroutine)」と、ゴルーチン間の安全な通信を可能にする「チャネル(channel)」を言語レベルでサポートすることで、並行処理を容易にしています。
- ゴルーチン:
go
キーワードを使って関数を呼び出すことで、新しいゴルーチンが生成され、その関数が並行して実行されます。 - チャネル: ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、データ競合を避けるための安全な方法として推奨されます("Don't communicate by sharing memory; share memory by communicating.")。
今回のケースでは、チャネルはイベントのストリームを受信するために使用されていますが、イベント数をカウントする変数自体はチャネルを介して共有されていなかったため、データ競合が発生していました。
技術的詳細
このコミットの技術的な核心は、共有変数eventsReceived
へのアクセスを、通常の整数操作からsync/atomic
パッケージのアトミック操作に置き換えることで、データ競合を解消した点にあります。
元のコードでは、eventsReceived
は単なるint
型の変数として宣言され、eventsReceived++
という操作でインクリメントされていました。この++
操作は、実際には以下の3つのステップから構成されます。
eventsReceived
の現在の値をメモリから読み込む。- 読み込んだ値に1を加算する。
- 新しい値を
eventsReceived
にメモリに書き込む。
複数のゴルーチンが同時にこの操作を実行しようとすると、例えばゴルーチンAが値を読み込んだ直後にゴルーチンBが値を読み込み、それぞれが加算して書き込むと、最終的な値が期待よりも小さくなる可能性があります(更新が失われる)。
修正後のコードでは、以下の変更が行われました。
eventsReceived
の型変更:var eventsReceived = 0
からvar eventsReceived int32 = 0
に変更されました。sync/atomic
パッケージの関数は、int32
やint64
のような特定のサイズの整数型を引数に取ります。- インクリメント操作の変更:
eventsReceived++
がatomic.AddInt32(&eventsReceived, 1)
に置き換えられました。atomic.AddInt32
は、指定されたint32
型変数(ポインタで渡す)に指定された値をアトミックに加算し、その結果を返します。この操作は不可分であるため、複数のゴルーチンが同時に呼び出しても、すべての加算が正しく反映され、更新が失われることはありません。 - 値の読み込み操作の変更:
if eventsReceived == 0
がif atomic.LoadInt32(&eventsReceived) == 0
に置き換えられました。atomic.LoadInt32
は、指定されたint32
型変数(ポインタで渡す)の値をアトミックに読み込みます。これにより、読み込み操作自体も他の書き込み操作と競合することなく、常に最新の正確な値を取得できます。
これらの変更により、eventsReceived
変数へのすべてのアクセス(読み込みと書き込み)がアトミック操作によって保護され、テスト中のデータ競合が完全に解消されました。
コアとなるコードの変更箇所
--- a/src/pkg/exp/inotify/inotify_linux_test.go
+++ b/src/pkg/exp/inotify/inotify_linux_test.go
@@ -9,6 +9,7 @@ package inotify
import (
"io/ioutil"
"os"
+ "sync/atomic"
"testing"
"time"
)
@@ -43,13 +44,13 @@ func TestInotifyEvents(t *testing.T) {
// Receive events on the event channel on a separate goroutine
eventstream := watcher.Event
- var eventsReceived = 0
+ var eventsReceived int32 = 0
done := make(chan bool)
go func() {
for event := range eventstream {
// Only count relevant events
if event.Name == testFile {
- eventsReceived++
+ atomic.AddInt32(&eventsReceived, 1)
t.Logf("event received: %s", event)
} else {
t.Logf("unexpected event received: %s", event)
@@ -67,7 +68,7 @@ func TestInotifyEvents(t *testing.T) {
// We expect this event to be received almost immediately, but let's wait 1 s to be sure
time.Sleep(1 * time.Second)
- if eventsReceived == 0 {
+ if atomic.LoadInt32(&eventsReceived) == 0 {
t.Fatal("inotify event hasn't been received after 1 second")
}
コアとなるコードの解説
1. import "sync/atomic"
の追加
+ "sync/atomic"
sync/atomic
パッケージの関数を使用するために、インポート宣言に追加されています。
2. eventsReceived
の型変更
- var eventsReceived = 0
+ var eventsReceived int32 = 0
eventsReceived
変数の型が、通常のint
(またはデフォルトのint
)からint32
に明示的に変更されました。これは、sync/atomic
パッケージの関数がint32
やint64
などの特定のサイズの整数型を操作するためです。
3. イベントカウントのインクリメント
- eventsReceived++
+ atomic.AddInt32(&eventsReceived, 1)
イベントが受信された際にeventsReceived
をインクリメントする部分が変更されました。
- 変更前:
eventsReceived++
は、非アトミックな読み込み、加算、書き込みの3ステップからなる操作であり、複数のゴルーチンから同時にアクセスされるとデータ競合が発生する可能性がありました。 - 変更後:
atomic.AddInt32(&eventsReceived, 1)
は、eventsReceived
変数のアドレスを渡し、それに1
をアトミックに加算します。この操作は不可分であり、他のゴルーチンからの干渉を受けずに安全に実行されるため、データ競合が防止されます。
4. イベントカウントのチェック
- if eventsReceived == 0 {
+ if atomic.LoadInt32(&eventsReceived) == 0 {
テストの最後にeventsReceived
の値をチェックする部分が変更されました。
- 変更前:
eventsReceived == 0
は、非アトミックな読み込み操作であり、eventsReceived
が別のゴルーチンによって同時に更新されている場合、古い値や部分的に更新された値を読み取ってしまう可能性がありました。 - 変更後:
atomic.LoadInt32(&eventsReceived)
は、eventsReceived
変数のアドレスを渡し、その値をアトミックに読み込みます。これにより、常に最新かつ正確な値が取得され、テストの信頼性が向上します。
これらの変更により、eventsReceived
変数へのすべてのアクセスがアトミック操作によって保護され、テスト中のデータ競合が解消されました。
関連リンク
- GitHub上のコミットページ: https://github.com/golang/go/commit/f5f3c3fe093fc359045a3818d3cd04f7b40b06c2
- Go言語のIssueトラッカー (一般的なリンク): https://github.com/golang/go/issues
参考にした情報源リンク
- Go言語
sync/atomic
パッケージのドキュメント: https://pkg.go.dev/sync/atomic - Go言語のメモリモデル (データ競合に関する詳細): https://go.dev/ref/mem
- Linux
inotify
manページ (英語): https://man7.org/linux/man-pages/man7/inotify.7.html