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

[インデックス 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イベントを読み取る)の両方から同時にアクセスされる可能性がありました。AddWatchpathsマップに書き込みを行い、readEventspathsマップから読み込みを行います。これらの操作が同期なしに行われると、データ競合が発生し、不正な値の読み込みやマップの破損につながる可能性がありました。

Fixes #3713という記述から、この問題がGoのIssue Trackerで報告されたバグであることがわかります。このバグ報告が、今回のデータ競合修正の直接的なトリガーとなりました。

前提知識の解説

inotify

inotifyはLinuxカーネルが提供する機能で、ファイルシステム上のイベント(ファイルの作成、削除、変更、移動など)を監視するために使用されます。アプリケーションはinotifyを通じて特定のファイルやディレクトリを監視対象として登録し、イベントが発生するとカーネルから通知を受け取ることができます。これにより、リアルタイムでのファイルシステム変更への対応が可能になります。

Go言語における並行処理とデータ競合

Go言語は、ゴルーチン(goroutine)とチャネル(channel)という強力なプリミティブを提供することで、並行処理を容易に記述できるように設計されています。ゴルーチンは非常に軽量なスレッドのようなもので、数千、数万のゴルーチンを同時に実行することが可能です。

しかし、並行処理を扱う際には「データ競合」という問題に注意が必要です。データ競合は以下の3つの条件がすべて満たされた場合に発生します。

  1. 複数のゴルーチンが同じメモリ領域にアクセスする。
  2. 少なくとも1つのアクセスが書き込み操作である。
  3. それらのアクセスが同期されていない。

データ競合が発生すると、プログラムの動作が非決定論的になり、デバッグが非常に困難になります。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()が追加されています。これにより、readEventspathsマップを読み取っている間は、AddWatchがマップに書き込むことができなくなり、データ競合が防止されます。

この修正により、AddWatchreadEventsという2つの並行に動作するコードパスが、Watcher構造体の共有マップ(watchespaths)に安全にアクセスできるようになり、データ競合によるバグが解消されます。

コアとなるコードの変更箇所

src/pkg/exp/inotify/inotify_linux.goファイルに以下の変更が加えられました。

  1. Watcher構造体にmu sync.Mutexフィールドが追加されました。
  2. Watcher.AddWatchメソッド内で、syscall.InotifyAddWatch呼び出しとw.watches/w.pathsマップへの書き込みの前後でw.mu.Lock()w.mu.Unlock()が追加されました。
  3. Watcher.readEventsメソッド内で、w.pathsマップからの読み込みの前後でw.mu.Lock()w.mu.Unlock()が追加されました。

コアとなるコードの解説

このコミットの核心は、sync.Mutexを用いた共有リソース(Watcher構造体のwatchespathsマップ)へのアクセス同期です。

Watcher構造体は、inotifyファイルディスクリプタ(fd)、監視対象のパスとそれに対応するウォッチディスクリプタを管理するマップ(watchespaths)、そしてイベントやエラーを送信するためのチャネルなど、inotify監視に必要な状態を保持しています。

AddWatchメソッドは、新しいパスを監視対象に追加する際に、pathsマップに新しいエントリを書き込みます。一方、readEventsゴルーチンは、カーネルからinotifyイベントを非同期に読み取り、そのイベントに含まれるウォッチディスクリプタ(raw.Wd)を使ってpathsマップから対応するパス名を取得します。

これらの操作が同時に行われると、AddWatchpathsマップを更新している最中にreadEventsがそのマップを読み取ろうとする、あるいはその逆の状況が発生し、データ競合を引き起こします。

sync.Mutex (w.mu) を導入することで、この問題が解決されます。

  • w.mu.Lock()を呼び出すと、そのゴルーチンはミューテックスの所有権を獲得します。もし他のゴルーチンが既にミューテックスをロックしている場合、現在のゴルーチンはロックが解放されるまで待機します。
  • w.mu.Unlock()を呼び出すと、ミューテックスの所有権を解放し、他の待機中のゴルーチンがロックを取得できるようになります。

AddWatchreadEventsの両方で、watchesまたはpathsマップにアクセスするクリティカルセクションの前後でw.mu.Lock()w.mu.Unlock()が呼び出されることで、これらのマップへのアクセスが排他的になります。つまり、一度に1つのゴルーチンだけがマップを操作できるようになり、データ競合が効果的に防止されます。

このパターンは、Go言語における共有メモリへの安全な並行アクセスを実現するための基本的な手法であり、"Don't communicate by sharing memory; share memory by communicating" (メモリを共有して通信するのではなく、通信によってメモリを共有する) というGoの並行処理の哲学とは異なるアプローチですが、特定の状況(ここではマップへの排他的アクセス)ではsync.Mutexが適切かつ効率的な解決策となります。

関連リンク

参考にした情報源リンク