FileSystemWatcher不是开箱即用的黑盒,必须规避事件重复、漏报、IO异常等坑:需防抖去重、异步处理、文件锁规避、路径可靠性校验及权威源仲裁。

FileSystemWatcher 不是“一设就灵”的黑盒,直接用默认配置做同步,90% 会出问题:事件重复、漏报、IOException 频发、一启动就崩。 它只负责通知“有变化”,不保证“变化已完成”“文件可读”“路径可靠”。真正能落地的同步逻辑,必须绕过它的坑,而不是调高 InternalBufferSize 或重试三次就完事。
Created 事件为什么总触发两次?
这不是 bug,是 Windows 文件系统 + 编辑器保存机制共同作用的结果。VS Code、记事本、Notepad++ 等多数工具保存时会先创建空文件(触发 Created),再写入内容(触发 Changed),或用临时文件 + 原子重命名(触发 Created + Renamed)。
- 别只订阅
Created,优先监听Changed事件,并配合NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size - 在回调里加简单防抖:记录上一次处理时间,200ms 内重复触发直接 return
- 避免直接
File.ReadAllText(e.FullPath)—— 此时文件极可能仍被编辑器句柄占用,抛IOException: The process cannot access the file - 真要读,用
FileStream以FileShare.Read打开,或加最多 2 次重试(间隔 50ms)
监控 OneDrive / UNC 路径为什么总失效?
FileSystemWatcher 依赖 Windows 内核 API ReadDirectoryChangesW,而 OneDrive、Google Drive、\server\share 这类路径不提供稳定内核通知——它们走的是用户态同步代理,事件延迟高、丢失率大,甚至完全静默。
- UNC 路径必须映射为本地驱动器号(如
Z:),且确保运行账户能访问该映射(服务账户无法继承交互式用户的映射) - OneDrive 场景放弃
FileSystemWatcher,改用轮询:Directory.GetFiles(path, filter, SearchOption.TopDirectoryOnly)+FileInfo.LastWriteTimeUtc比对,间隔至少 3 秒 - 绝对不要设置
IncludeSubdirectories = true监控整个 OneDrive 根目录——数万文件会瞬间打爆缓冲区,触发Error事件后监听器静默停止 - 轮询时用
new DirectoryInfo(path).GetFiles()替代Directory.GetFiles(),前者支持SearchOption.AllDirectories且异常更可控
Changed 事件里读文件总失败,怎么破?
事件到达 ≠ 文件写完 ≠ 句柄释放。Excel、Photoshop、日志组件等长期持有文件锁,此时强行读必然失败。这不是你代码的问题,是竞态条件的本质限制。
- 永远不要在事件回调中同步读取刚变更的文件;改为投递到线程池任务(
Task.Run(() => ProcessFile(e.FullPath))),留出写入窗口 - 读取前先检查
File.GetLastWriteTimeUtc()是否比上次记录更新,再尝试打开;若失败,用FileStream指定FileAccess.Read和FileShare.Read | FileShare.Delete - 大文件别用
File.ReadAllBytes(),改用流式哈希(SHA256.HashData(fs))或分块读取,防 OOM 和长时阻塞 - 写入目标文件必须原子:先
File.WriteAllBytes(tmpPath, bytes),再File.Move(tmpPath, targetPath, true)
如何避免同步逻辑陷入死循环?
双向同步时,A 改 → 触发同步写 B → B 改 → 又触发同步写 A,无限套娃。FileSystemWatcher 不区分“谁改的”,只管“改了没”。
- 必须约定权威源:比如本地是 source,远端是 readonly target;或按时间戳/哈希值仲裁(谁新谁赢)
- 同步写入前,在目标路径旁写一个标记文件(如
.syncing),处理前先检查是否存在,存在则跳过本次 - 用
FileSystemWatcher的EnableRaisingEvents = false临时关闭监听,写完再打开,但注意异常时务必恢复 - 日志里记录
e.ChangeType、e.FullPath、DateTime.UtcNow和操作结果,否则出问题根本没法回溯是哪次触发导致的连锁失败
最常被忽略的一点:所有事件回调都是在 IO 线程池里执行的,没做 await / 异步等待,也没加锁或队列控制,高频写入(如编译输出、日志滚动)下极易丢事件、堆栈溢出、或引发 InvalidOperationException。别指望靠增大 InternalBufferSize 解决——它只是把问题从“丢事件”变成“内存爆掉”。真正的可靠性,来自设计层面的防抖、去重、异步解耦和明确的失败策略。


















