FileSystemWatcher 默认仅监控当前目录且存在多处盲区,需显式设置IncludeSubdirectories、NotifyFilter等属性,并注意事件触发时机、线程安全、资源释放等问题。

FileSystemWatcher 能监听创建和删除,但默认不触发
直接 new FileSystemWatcher 并设置 EnableRaisingEvents = true 后,你很可能收不到 Created 或 Deleted 事件——不是代码写错了,而是它默认只监视当前目录,且对子目录、重命名、临时文件等行为有天然盲区。
常见错误现象:Deleted 事件在资源管理器里按 Delete 键时不触发(实际是移到回收站,文件没真删);用记事本保存时触发两次 Created(先写临时文件再重命名);监控 C:Temp 却收不到 C:TempSubile.txt 的事件。
- 必须显式设置
IncludeSubdirectories = true才能监听子目录 -
NotifyFilter至少要包含NotifyFilters.FileName | NotifyFilters.DirectoryName,否则重命名或移动操作不会触发Created/Deleted - 避免监听根目录(如
C:)或系统目录,容易因权限不足静默失败,建议从具体业务路径开始,比如AppDomain.CurrentDomain.BaseDirectory + "watch"
Created 和 Deleted 事件的触发时机很“物理”
它们反映的是文件系统层面的原子操作,不是用户意图。比如用 VS Code 保存文件,常看到 Created → Changed → Renamed 一连串事件;用拖拽方式移动文件到监控目录,会触发 Created(而非 Moved),因为底层是复制+删除两步。
使用场景:日志归档、上传队列扫描、配置热加载。不适合做“用户操作审计”,因为无法区分“新建文档”和“程序生成缓存”。
-
Created在文件句柄首次被打开写入时就可能触发(哪怕还没写完),所以事件回调里读文件大概率报IOException: 文件正由另一进程使用 -
Deleted只在文件被DeleteFile()系统调用真正移除时触发,回收站、剪切粘贴、Git clean 都不走这条路 - 若需稳定捕获“内容落地”,应在
Changed事件后加延迟(如Task.Delay(100))再读,或监听LastWrite时间戳变化
多线程和事件丢失是真实存在的坑
FileSystemWatcher 内部用 Windows API 的 ReadDirectoryChangesW,事件是异步投递到 .NET 线程池的。高频率文件操作(如解压百个文件)下,很容易丢事件,且回调不在 UI 线程——直接更新 WinForm 控件会抛异常。
性能影响:每个实例占用一个内核句柄,大量 watcher 会耗尽句柄数;监控网络路径(\servershare)时延迟高、易断连。
- 务必在事件处理方法开头加
try/catch,异常会直接终止该 watcher 的后续事件 - 不要在事件回调里做耗时操作(如上传、解析 JSON),用
Task.Run()拆出去 - 监听同一目录时,多个 watcher 实例之间不共享事件队列,但共用底层 OS 缓冲区,反而更容易丢事件——一个就够了
- 检查
InternalBufferSize(默认 8KB),大流量场景可设为 64KB,但超过 64KB 可能引发内存碎片
Dispose 不及时会导致句柄泄漏和后台线程残留
忘记调用 Dispose() 或没在 using 块里用 FileSystemWatcher,程序退出后仍可能有未释放的内核对象,任务管理器里看句柄数缓慢上涨。更隐蔽的问题是:即使已停用(EnableRaisingEvents = false),只要没 Dispose,内部线程仍在轮询缓冲区。
容易被忽略的地方:在 ASP.NET Core 中注册为 Singleton 服务时,必须实现 IAsyncDisposable 并在 DisposeAsync 里调用 watcher.Dispose();WinForm 关闭窗体前,要在 FormClosing 里手动 Dispose。
- 推荐写法:
using var watcher = new FileSystemWatcher(path); watcher.Created += OnCreated; watcher.EnableRaisingEvents = true;
- 如果 watcher 生命周期长于作用域(比如后台服务),一定要在服务停止逻辑中明确调用
watcher.Dispose() - 别依赖终结器——它不保证何时运行,且无法释放内核句柄










