C# FileSystemWatcher复制文件时文件被其他进程占用如何解决
问题核心原因
你遇到的占用异常不是自身代码未释放文件资源导致的,三个核心诱因:
FileSystemWatcher的Changed事件触发时机是目标文件开始写入时,而非写入完成时,此时写入文件的进程(比如文本编辑器、游戏存盘进程)持有文件锁,默认的File.Copy方法申请文件访问权限时会直接被系统拒绝。- 原生
File.Copy默认以禁止其他进程写入的共享模式打开源文件,只要源文件被其他进程以写入模式打开,就会直接抛出IO异常,没有内置重试、兼容共享访问的逻辑。 - 你当前代码里的全局变量
makeonce没有做线程安全保护,FileSystemWatcher的事件由线程池触发,多事件并发时会出现目录重复创建、目标路径被覆盖的问题。
注意:你没有权限"释放其他进程持有的文件锁",这类锁归属持有文件句柄的外部进程,强行通过系统API关闭其他进程的句柄会导致对方数据损坏、进程崩溃,绝对不要用这类方案。
可直接落地的修复方案
核心思路是:改用支持共享读写的文件流手动复制,搭配短间隔重试机制,在文件可访问时持续覆盖写入目标位置,同时修复线程安全问题。
- 移除原有的无保护全局布尔值判断,加线程锁保证目标目录只创建一次
- 放弃原生
File.Copy,手动创建文件流时指定FileShare.ReadWrite标记,允许源文件被其他进程同时读写,从根源避开大部分占用锁问题 - 加入重试逻辑,遇到暂时的占用问题时等待几百毫秒再尝试,直到达到最大重试次数
- 复制逻辑全程不持有长期锁,复制完成立刻释放流资源
修正后的代码实现如下:
// 根据实际监听的文件大小调整参数 private const int MaxRetryTimes = 15; private const int RetryWaitMs = 300; private readonly object _pathInitLock = new object(); private string _targetSavePath = string.Empty; private void CopyFileOnChanged(string sourceDir, string sourceFullPath) { // 保证目标目录只初始化一次,避免并发问题 lock (_pathInitLock) { if (string.IsNullOrEmpty(_targetSavePath)) { string dateStr = DateTime.Now.ToString("ddd MM.dd.yyyy"); string timeStr = DateTime.Now.ToString("HH.mm tt"); string saveFolderName = string.Format("Save Game {0} {1}", dateStr, timeStr); string saveFolderFullPath = Path.Combine(sourceDir, saveFolderName); Directory.CreateDirectory(saveFolderFullPath); string sourceFileName = Path.GetFileName(sourceFullPath); _targetSavePath = Path.Combine(saveFolderFullPath, sourceFileName); } } // 带重试的复制逻辑 for (int retryCount = 0; retryCount < MaxRetryTimes; retryCount++) { try { // 核心:以共享读写模式打开源文件,允许其他进程同时写入 using (FileStream sourceStream = new FileStream( path: sourceFullPath, mode: FileMode.Open, access: FileAccess.Read, share: FileShare.ReadWrite)) using (FileStream targetStream = new FileStream( path: _targetSavePath, mode: FileMode.Create, access: FileAccess.Write, share: FileShare.None)) { sourceStream.CopyTo(targetStream); } // 本次复制成功,直接返回 return; } catch (IOException) { // 等待指定时间后重试 Thread.Sleep(RetryWaitMs); } } // 超过最大重试次数输出日志,不抛出异常中断文件监听 PrintErr($"文件 {sourceFullPath} 重试{MaxRetryTimes}次仍复制失败,可能被长期独占占用"); }
额外优化提示
- 如果你监听的是大文件(比如GB级的游戏存档、视频文件),可以把
MaxRetryTimes调到20以上,RetryWaitMs调到500毫秒,给写入进程留足完成写入的时间。 FileSystemWatcher对同一个文件的单次写入会触发多次Changed事件,这套重试覆盖逻辑天然兼容这个特性,每次触发都会把当前最新的文件内容覆盖到备份目录,完全符合你"文件变更完成前持续重复覆盖复制"的需求。- 如果需要监听多个文件的变更,不要用全局的
_targetSavePath变量,改用字典按源文件路径存储对应的备份目标路径即可。
内容的提问来源于stack exchange,提问作者Daniel Lip
相关产品推荐
相关产品推荐

