幽霊スレッドを飼い慣らせ:FileSystemWatcherによる高負荷ファイル監視の深淵
多くのエンジニアが `FileSystemWatcher` を使い、そして挫折する。「イベントが二重に発火する」「メモリリークでプロセスが肥大化する」「UIがフリーズする」。これらは全て、Windowsのファイルシステムイベントの性質と、.NETのガベージコレクション(GC)の作法を理解していないことに起因する「人災」だ。
本稿では、レガシーなWindows Forms環境において、安定して数万件のログを捌き切るための「極限の監視アーキテクチャ」を伝授する。
—
1. FileSystemWatcherの「嘘」を見抜く
`FileSystemWatcher` は魔法ではない。OSのバッファオーバーフローという「物理的な限界」がある。
- イベントの重複: 1回のファイル保存で `Changed` が複数回飛ぶのは仕様だ。OSが「属性変更」「書き込み終了」を個別に通知するからだ。これを個別に処理していては、UIスレッドが死ぬ。
- バッファ不足: 監視対象がネットワークドライブの場合、LANの瞬断や高負荷時にイベントが取りこぼされる。
対策:デバウンス(Debounce)パターンの実装
イベントを即時実行せず、タイマーを介して「一定時間静寂が続いた後に処理する」のが鉄則だ。
‘ 監視処理の核となるDebounceタイマー
Private WithEvents _debounceTimer As New System.Windows.Forms.Timer() With {.Interval = 500}
Private _pendingFilePath As String
Private Sub FileSystemWatcher_Changed(sender As Object, e As FileSystemEventArgs) Handles watcher.Changed
‘ イベント発生のたびにタイマーをリセットし、スパイクを抑制する
_pendingFilePath = e.FullPath
_debounceTimer.Stop()
_debounceTimer.Start()
End Sub
Private Sub _debounceTimer_Tick(sender As Object, e As EventArgs) Handles _debounceTimer.Tick
_debounceTimer.Stop()
‘ ここで初めてファイル解析を行う
ProcessLogFile(_pendingFilePath)
End Sub
—
2. UIスレッドの「絶対不可侵領域」を守る
`FileSystemWatcher` は、UIスレッドとは全く別のバックグラウンドスレッドでイベントを発生させる。ここで `DataGridView.Rows.Add` を叩けば、例外の発生か、最悪の場合プロセスがクラッシュする。
究極の回避策:Invokeと同期コンテキスト
`Control.Invoke` は便利だが、多用するとメッセージキューが詰まる。可能な限りデータをバッファリングし、一括でUIに流し込むのがプロの流儀だ。
Private Sub ProcessLogFile(filePath As String)
‘ 別スレッドで重い読み込みを実行
Dim lines = ReadFileSafe(filePath) ‘ ロック対策済みの読み込みメソッド
‘ UIスレッドへ安全に引き渡す
Me.Invoke(Sub()
‘ グリッドの更新は必ずここで行う
UpdateGrid(lines)
End Sub)
End Sub
—
3. レガシー環境でのメモリ解放の極意
.NETはGCに依存しているが、ファイルストリームやWin32リソースを扱う場合、`Dispose` を待つ余裕はない。
- FileStreamの共有モード: ログを解析する際、書き込み中のファイルを掴んでしまうと `IOException` が発生する。`FileShare.ReadWrite` を指定し、排他制御の罠を回避せよ。
- 明示的Dispose: `FileSystemWatcher` 自体もリソースを食う。フォームを閉じる際は必ず `Dispose()` を明示的に呼び出し、イベントハンドラを解除する。これを怠れば、アプリを閉じた後もプロセスがゾンビとして残り続ける。
Protected Overrides Sub Dispose(disposing As Boolean)
If disposing Then
If watcher IsNot Nothing Then
watcher.EnableRaisingEvents = False
RemoveHandler watcher.Changed, AddressOf FileSystemWatcher_Changed
watcher.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub
—
4. 現場で生き残るための「鉄則」
1. フィルターを絞れ: `Filter = “.log”` は必須。ディレクトリ全体を監視してはいけない。
2. NotifyFilterの調整: `NotifyFilters.LastWrite` だけで十分なケースが9割。`FileName` や `Attributes` まで監視すると、CPU負荷が跳ね上がる。
3. エラーハンドリング: 監視対象のフォルダが突然削除された場合、`FileSystemWatcher` は黙って死ぬ。`Error` イベントを購読し、監視の再起動ロジックを必ず組み込め。
結びに代えて
VB.NETという言語は、しばしば「古臭い」と揶揄される。しかし、WindowsのAPIを直接叩き、OSの深淵を制御できるこの言語の力は、現代の抽象化されたフレームワークにはない強みだ。
ファイル監視一つとっても、その背景にあるスレッドモデル、メモリ管理、そしてUIのレスポンスを制御しきることで、システムは「動く」から「安定して運用できる」へと昇華する。あなたが今書いているその数行のコードが、数年後の保守担当者の命を救うことになる。
極限まで無駄を削ぎ落とし、リソースの寿命を管理せよ。それこそが、伝説のエンジニアに近づくための唯一の道だ。
