[インデックス 13387] ファイルの概要
このコミットは、Go言語の実験的なexp/inotify
パッケージにおけるデータ競合(data race)を修正するものです。具体的には、inotify_linux.go
ファイル内のWatcher
構造体に対してsync.Mutex
を追加し、ファイル監視の追加(AddWatch
)およびイベント読み取り(readEvents
)の処理において、共有リソースへのアクセスを同期させることで、競合状態を防いでいます。
コミット
commit b3382ec9e9cfbb20efd7bf7d6a369071a46c8dfe
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date: Mon Jun 25 14:08:09 2012 -0400
exp/inotify: prevent data race
Fixes #3713.
R=bradfitz, rsc
CC=golang-dev
https://golang.org/cl/6331055
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b3382ec9e9cfbb20efd7bf7d6a369071a46c8dfe
元コミット内容
exp/inotify: prevent data race
Fixes #3713.
変更の背景
このコミットは、Go言語の実験的なexp/inotify
パッケージにおいて報告されたデータ競合の問題を解決するために行われました。inotify
はLinuxカーネルが提供するファイルシステムイベント監視メカニズムであり、exp/inotify
パッケージはそのGo言語バインディングを提供します。
データ競合は、複数のゴルーチン(Goの軽量スレッド)が同時に共有データにアクセスし、そのうち少なくとも1つが書き込み操作であり、かつそれらのアクセスが適切に同期されていない場合に発生します。このような状況では、プログラムの実行結果が予測不能になったり、クラッシュしたりする可能性があります。
この特定のケースでは、Watcher
構造体のpaths
マップ(監視対象のパスを管理するマップ)が、AddWatch
メソッド(新しい監視を追加する)とreadEvents
ゴルーチン(inotifyイベントを読み取る)の両方から同時にアクセスされる可能性がありました。AddWatch
はpaths
マップに書き込みを行い、readEvents
はpaths
マップから読み込みを行います。これらの操作が同期なしに行われると、データ競合が発生し、不正な値の読み込みやマップの破損につながる可能性がありました。
Fixes #3713
という記述から、この問題がGoのIssue Trackerで報告されたバグであることがわかります。このバグ報告が、今回のデータ競合修正の直接的なトリガーとなりました。
前提知識の解説
inotify
inotify
はLinuxカーネルが提供する機能で、ファイルシステム上のイベント(ファイルの作成、削除、変更、移動など)を監視するために使用されます。アプリケーションはinotify
を通じて特定のファイルやディレクトリを監視対象として登録し、イベントが発生するとカーネルから通知を受け取ることができます。これにより、リアルタイムでのファイルシステム変更への対応が可能になります。
Go言語における並行処理とデータ競合
Go言語は、ゴルーチン(goroutine)とチャネル(channel)という強力なプリミティブを提供することで、並行処理を容易に記述できるように設計されています。ゴルーチンは非常に軽量なスレッドのようなもので、数千、数万のゴルーチンを同時に実行することが可能です。
しかし、並行処理を扱う際には「データ競合」という問題に注意が必要です。データ競合は以下の3つの条件がすべて満たされた場合に発生します。
- 複数のゴルーチンが同じメモリ領域にアクセスする。
- 少なくとも1つのアクセスが書き込み操作である。
- それらのアクセスが同期されていない。
データ競合が発生すると、プログラムの動作が非決定論的になり、デバッグが非常に困難になります。Go言語では、データ競合を検出するためのツール(Go Race Detector)も提供されています。
sync.Mutex
sync.Mutex
はGo言語の標準ライブラリsync
パッケージで提供される相互排他ロック(mutual exclusion lock)です。これは、共有リソースへのアクセスを同期させるための最も基本的なメカニズムの一つです。
Lock()
: ミューテックスをロックします。既にロックされている場合、Lock()
を呼び出したゴルーチンはロックが解放されるまでブロックされます。Unlock()
: ミューテックスをアンロックします。これにより、他のゴルーチンがロックを取得できるようになります。
sync.Mutex
を使用することで、クリティカルセクション(共有データにアクセスするコードの領域)に一度に1つのゴルーチンしか入ることができないように保証し、データ競合を防ぐことができます。
技術的詳細
このコミットの技術的詳細は、exp/inotify
パッケージのWatcher
構造体とその関連メソッドにおけるsync.Mutex
の導入に集約されます。
Watcher
構造体へのsync.Mutex
の追加
type Watcher struct {
mu sync.Mutex // 追加
fd int // File descriptor (as returned by the inotify_init() syscall)
watches map[string]*watch // Map of inotify watches (key: path)
paths map[int]string // Map of watched paths (key: watch descriptor)
event chan *Event // Kernel events are pushed to this channel
error chan error // Errors are pushed to this channel
done chan bool // Used to signal the readEvents goroutine to exit
isClosed bool // Set to true when the Watcher is closed
}
Watcher
構造体にmu sync.Mutex
フィールドが追加されました。このミューテックスは、Watcher
インスタンス内の共有リソース、特にwatches
マップとpaths
マップへのアクセスを保護するために使用されます。
AddWatch
メソッドにおけるロック
AddWatch
メソッドは、新しいファイル監視を追加する際にpaths
マップに書き込みを行います。この操作がreadEvents
ゴルーチンによるpaths
マップの読み込みと同時に行われるとデータ競合が発生するため、ミューテックスによる保護が導入されました。
func (w *Watcher) AddWatch(path string, flags uint32) error {
// ... (既存のコード) ...
w.mu.Lock() // synchronize with readEvents goroutine
wd, err := syscall.InotifyAddWatch(w.fd, path, flags)
if err != nil {
w.mu.Unlock() // エラー発生時はロックを解放
return &os.PathError{
Op: "inotify_add_watch",
Path: path,
Err: err,
}
}
// ... (既存のコード) ...
w.watches[path] = &watch{wd: uint32(wd), flags: flags}
w.paths[wd] = path
w.mu.Unlock() // 処理完了後にロックを解放
return nil
}
syscall.InotifyAddWatch
の呼び出し前と、w.watches
およびw.paths
マップへの書き込み処理の前後でw.mu.Lock()
とw.mu.Unlock()
が呼び出されています。これにより、AddWatch
メソッドがpaths
マップを操作している間は、他のゴルーチン(特にreadEvents
)がマップにアクセスできないように排他制御が行われます。エラーが発生した場合も、ロックが確実に解放されるようにdefer
ではなく明示的なUnlock
が使用されています。
readEvents
ゴルーチンにおけるロック
readEvents
ゴルーチンは、inotifyイベントをカーネルから読み取り、そのイベント情報に基づいてpaths
マップから監視対象のパス名を取得します。この読み込み操作も、AddWatch
による書き込みと競合する可能性があるため、ミューテックスで保護されます。
func (w *Watcher) readEvents() {
// ... (既存のコード) ...
for {
// ... (イベントの読み込み) ...
// doesn't append the filename to the event, but we would like to always fill the
// the "Name" field with a valid filename. We retrieve the path of the watch from
// the "paths" map.
w.mu.Lock() // ロックを取得
event.Name = w.paths[int(raw.Wd)]
w.mu.Unlock() // ロックを解放
if nameLen > 0 {
// ... (ファイル名の処理) ...
}
// ... (イベントのチャネルへの送信) ...
}
}
event.Name = w.paths[int(raw.Wd)]
という行でpaths
マップから読み込みを行う直前と直後にw.mu.Lock()
とw.mu.Unlock()
が追加されています。これにより、readEvents
がpaths
マップを読み取っている間は、AddWatch
がマップに書き込むことができなくなり、データ競合が防止されます。
この修正により、AddWatch
とreadEvents
という2つの並行に動作するコードパスが、Watcher
構造体の共有マップ(watches
とpaths
)に安全にアクセスできるようになり、データ競合によるバグが解消されます。
コアとなるコードの変更箇所
src/pkg/exp/inotify/inotify_linux.go
ファイルに以下の変更が加えられました。
Watcher
構造体にmu sync.Mutex
フィールドが追加されました。Watcher.AddWatch
メソッド内で、syscall.InotifyAddWatch
呼び出しとw.watches
/w.paths
マップへの書き込みの前後でw.mu.Lock()
とw.mu.Unlock()
が追加されました。Watcher.readEvents
メソッド内で、w.paths
マップからの読み込みの前後でw.mu.Lock()
とw.mu.Unlock()
が追加されました。
コアとなるコードの解説
このコミットの核心は、sync.Mutex
を用いた共有リソース(Watcher
構造体のwatches
とpaths
マップ)へのアクセス同期です。
Watcher
構造体は、inotifyファイルディスクリプタ(fd
)、監視対象のパスとそれに対応するウォッチディスクリプタを管理するマップ(watches
とpaths
)、そしてイベントやエラーを送信するためのチャネルなど、inotify監視に必要な状態を保持しています。
AddWatch
メソッドは、新しいパスを監視対象に追加する際に、paths
マップに新しいエントリを書き込みます。一方、readEvents
ゴルーチンは、カーネルからinotifyイベントを非同期に読み取り、そのイベントに含まれるウォッチディスクリプタ(raw.Wd
)を使ってpaths
マップから対応するパス名を取得します。
これらの操作が同時に行われると、AddWatch
がpaths
マップを更新している最中にreadEvents
がそのマップを読み取ろうとする、あるいはその逆の状況が発生し、データ競合を引き起こします。
sync.Mutex
(w.mu
) を導入することで、この問題が解決されます。
w.mu.Lock()
を呼び出すと、そのゴルーチンはミューテックスの所有権を獲得します。もし他のゴルーチンが既にミューテックスをロックしている場合、現在のゴルーチンはロックが解放されるまで待機します。w.mu.Unlock()
を呼び出すと、ミューテックスの所有権を解放し、他の待機中のゴルーチンがロックを取得できるようになります。
AddWatch
とreadEvents
の両方で、watches
またはpaths
マップにアクセスするクリティカルセクションの前後でw.mu.Lock()
とw.mu.Unlock()
が呼び出されることで、これらのマップへのアクセスが排他的になります。つまり、一度に1つのゴルーチンだけがマップを操作できるようになり、データ競合が効果的に防止されます。
このパターンは、Go言語における共有メモリへの安全な並行アクセスを実現するための基本的な手法であり、"Don't communicate by sharing memory; share memory by communicating" (メモリを共有して通信するのではなく、通信によってメモリを共有する) というGoの並行処理の哲学とは異なるアプローチですが、特定の状況(ここではマップへの排他的アクセス)ではsync.Mutex
が適切かつ効率的な解決策となります。
関連リンク
- Go言語の
sync
パッケージドキュメント: https://pkg.go.dev/sync - Go言語の
exp/inotify
パッケージドキュメント (当時のもの): https://pkg.go.dev/exp/inotify (現在はx/sys/unix
などに統合されている可能性が高い) - Linux
inotify
manページ: https://man7.org/linux/man-pages/man7/inotify.7.html
参考にした情報源リンク
- Go Issue #3713 (データ競合の報告): https://github.com/golang/go/issues/3713
- Go Code Review (CL) 6331055: https://golang.org/cl/6331055
- Go Concurrency Patterns: Mutexes: https://go.dev/blog/sync-mutex (GoブログのMutexに関する記事)
- A Tour of Go - Mutexes: https://go.dev/tour/concurrency/9
- Go Race Detector: https://go.dev/doc/articles/race_detector