如何让FileSystemWatcher在本地/远程文件复制完成后启动处理?
如何让FileSystemWatcher等待文件复制完成后再处理?
我写了一个程序,用StartFileWatcher监控C:\TEMP文件夹(包含子目录),根据文件的LastWriteTimeUtc把文件信息写入C# Channel,并做排序对比。多数情况下(文件快速复制完成时),文件能按LastWriteTime顺序处理,但如果复制耗时较长,顺序就会出错。目前我用手动设置延迟(await Task.Delay(5000, cancellationToken);)来处理,但如果复制超过5000ms就失效了。有没有办法不用手动延迟,让FileSystemWatcher等文件复制完成后再开始工作?
Main:
static async Task Main(string[] args) { string folderPath = @"C:\TEMP"; CancellationTokenSource cts = new CancellationTokenSource(); Channel<string> fileChannel = Channel.CreateUnbounded<string>(); // Start the file watcher Task watcherTask = StartFileWatcher( folderPath, fileChannel.Writer, cts.Token); // Start processing files Task processingTask = ProcessFilesAsync( fileChannel.Reader, cts.Token); // Wait for user input to stop Console.WriteLine("Press Enter to stop..."); Console.ReadLine(); // Cancel the tasks cts.Cancel(); // Wait for the tasks to complete await Task.WhenAll(watcherTask, processingTask); Console.WriteLine("Program stopped."); }
StartFileWatcher:
static async Task StartFileWatcher( string folderPath, ChannelWriter<string> channelWriter, CancellationToken cancellationToken) { using (var watcher = new FileSystemWatcher(folderPath)) { List<string> pendingFiles = new List<string>(); watcher.IncludeSubdirectories = true; watcher.Created += (sender, e) => { if (!e.Name.StartsWith("~")) { lock (pendingFiles) { pendingFiles.Add(e.FullPath); pendingFiles.Sort((a, b) => File.GetLastWriteTimeUtc(a) .CompareTo(File.GetLastWriteTimeUtc(b))); } } }; watcher.EnableRaisingEvents = true; while (!cancellationToken.IsCancellationRequested) { List<string> filesToProcess; lock (pendingFiles) { filesToProcess = new List<string>(pendingFiles); pendingFiles.Clear(); } foreach (var filePath in filesToProcess) { channelWriter.TryWrite(filePath); } // Adjust the delay as needed await Task.Delay(5000, cancellationToken); } } }
ProcessFilesAsync:
static async Task ProcessFilesAsync( ChannelReader<string> channelReader, CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { if (await channelReader.WaitToReadAsync(cancellationToken)) { while (channelReader.TryRead(out string filePath)) { Console.WriteLine($"file: {filePath}," + $"LastWriteTime: " + $"{new FileInfo(filePath).LastWriteTime}"); // Simulate processing delay await Task.Delay(2000, cancellationToken); } } } }
解决方案:检测文件锁定状态判断复制完成
FileSystemWatcher触发Created事件时,文件往往还处于写入/复制状态,此时读取LastWriteTime会不准确,直接处理也可能引发错误。可以通过尝试独占打开文件的方式,判断文件是否已完成写入,替代固定延迟方案。
修改后的核心逻辑:
- 新增待检测队列,临时存放刚创建的文件
- 定期检测队列中的文件,尝试独占打开验证是否解锁
- 解锁成功的文件加入排序队列,再写入Channel处理
修改后的StartFileWatcher代码:
static async Task StartFileWatcher( string folderPath, ChannelWriter<string> channelWriter, CancellationToken cancellationToken) { using (var watcher = new FileSystemWatcher(folderPath)) { // 线程安全的待检测文件队列 ConcurrentQueue<string> pendingCheckQueue = new ConcurrentQueue<string>(); // 已就绪的待处理文件列表(用于排序) List<string> pendingProcessList = new List<string>(); watcher.IncludeSubdirectories = true; watcher.Created += (sender, e) => { if (!e.Name.StartsWith("~")) { pendingCheckQueue.Enqueue(e.FullPath); } }; watcher.EnableRaisingEvents = true; while (!cancellationToken.IsCancellationRequested) { // 批量检测待验证文件 while (pendingCheckQueue.TryDequeue(out string filePath)) { if (IsFileReady(filePath)) { lock (pendingProcessList) { pendingProcessList.Add(filePath); // 按LastWriteTimeUtc排序 pendingProcessList.Sort((a, b) => File.GetLastWriteTimeUtc(a).CompareTo(File.GetLastWriteTimeUtc(b))); } } else { // 文件仍锁定,放回队列下次检测 pendingCheckQueue.Enqueue(filePath); } } // 处理已就绪的文件 List<string> filesToProcess; lock (pendingProcessList) { filesToProcess = new List<string>(pendingProcessList); pendingProcessList.Clear(); } foreach (var filePath in filesToProcess) { channelWriter.TryWrite(filePath); } // 短间隔轮询,平衡响应速度与资源占用 await Task.Delay(100, cancellationToken); } } } // 辅助方法:验证文件是否可独占访问(判断写入完成) private static bool IsFileReady(string filePath) { try { // 尝试以独占方式打开文件 using (FileStream stream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) { return true; } } catch (IOException) { // 文件被锁定,说明还在写入 return false; } catch (Exception) { // 文件不存在或其他异常,直接排除 return false; } }
优化说明:
- 用
ConcurrentQueue存储待检测文件,避免线程安全问题,无需额外加锁 - 轮询间隔缩短至100ms,既保证响应及时性,又不会占用过多系统资源
- 锁定的文件会重新放回队列,直到解锁或任务被取消,完全适配任意时长的复制操作
内容的提问来源于stack exchange,提问作者user584018
相关产品推荐
相关产品推荐

