C# 中的 FileStream 和 FileSystemWatcher,奇怪的问题“进程无法访问文件”

Posted

技术标签:

【中文标题】C# 中的 FileStream 和 FileSystemWatcher,奇怪的问题“进程无法访问文件”【英文标题】:FileStream and a FileSystemWatcher in C#, Weird Issue "process cannot access the file" 【发布时间】:2014-03-11 10:42:22 【问题描述】:

我有一个复杂的代码库,它正在侦听某个文件夹上的 FileCreated 事件。创建文件后(还包括将文件移动到该文件夹​​),我想读入该文件并对其进行处理。它适用于第一个文件,但在所有其他尝试之后抛出异常。在调试模式下(使用 VisualStudio)会抛出错误,但如果我只是单击“继续”.. 它就会工作(没有错误)。

我已经发布了简化的代码,它演示了这个问题。

比如你启动应用程序,点击“开始”按钮,然后“新建一个文本文件”

输出是:

Working

如果您随后以完全相同的方式创建第二个文件,则输出为:

Broken: The process cannot access the file 'C:\TestFolder\New Text Document (2).txt' because it is being used by another process.
Working, after breaking

查看我的代码后,您会看到上面的一组打印输出意味着首先抛出了“无法访问文件”异常,但在 catch 语句中执行相同的调用突然起作用了。

这对我来说毫无意义,因为该文件显然没有被其他任何东西使用(我刚刚创建它).. 无论如何它会在一秒钟后工作....

下面是我的代码

XAML:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" >
    <StackPanel>
        <Button Click="Button_Click"  Content="Start"/>
    </StackPanel>
</Window>

代码背后:

using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;


namespace WpfApplication1
 
    public partial class MainWindow : Window
    
        public MainWindow()
        
            InitializeComponent();
        

        private void Button_Click(object sender, RoutedEventArgs e)
        
            test();
        


        String Folder = @"C:\TestFolder";

        private void test()
        
            FileSystemWatcher watch = new FileSystemWatcher(Folder);
            watch.Created += new FileSystemEventHandler(FileCreated);
            watch.EnableRaisingEvents = true;

            Process.Start(Folder);
        

        private void FileCreated(object sender, FileSystemEventArgs fsEvent)
        

            if (File.Exists(fsEvent.FullPath))
            

                // Thread.Sleep(1000);// Sleeping for 1 second seems to prevent the error from happening...?
                // If i am debugging, and pause on exceptions... then it also suddenly works (similar to the Sleep above)
                try
                

                    FileStream fs = new FileStream(fsEvent.FullPath, FileMode.Open); 
                    Console.WriteLine("Working");
                    fs.Close();
                
                catch (IOException ex)
                
                    Console.WriteLine("Broken: " + ex.Message);
                    try
                                            
                        FileStream fs = new FileStream(fsEvent.FullPath, FileMode.Open);
                        Console.WriteLine("Working, after breaking");
                        fs.Close();

                    
                    catch(IOException ex2)
                                            
                        FileStream fs = new FileStream(fsEvent.FullPath, FileMode.Open);
                        Console.WriteLine("really broken: " + ex2.Message);
                        fs.Close();
                    
                


            
        
    

【问题讨论】:

【参考方案1】:

自 .NET 1.0 以来,我已经看到了您所描述的行为,并且从未费心找出它发生的原因。似乎操作系统或 .NET 有时(?)在您调用 close 和 dispose 后会在短时间内锁定文件。

我做了一个变通办法——或者如果你愿意,也可以破解——事实证明这对我们来说非常强大。我们每天在服务器场中处理数百万个文件,文件观察器检测到的所有文件在移交给进一步处理之前都会通过此方法。

它的作用是在文件上放置一个排他锁。如果失败,它会选择等待最多 10 秒以关闭文件,然后再放弃。

    public static bool IsFileClosed(string filepath, bool wait)
    
        bool        fileClosed = false;
        int         retries = 20;
        const int   delay = 500; // Max time spent here = retries*delay milliseconds

        if (!File.Exists(filepath))
            return false;

        do
        
            try 
            
                // Attempts to open then close the file in RW mode, denying other users to place any locks.
                FileStream fs = File.Open(filepath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
                fs.Close();
                fileClosed = true; // success
            
            catch (IOException) 

            if (!wait) break;

            retries --;

            if (!fileClosed)
                Thread.Sleep( delay );
        
        while (!fileClosed && retries > 0);

        return fileClosed;
    

【讨论】:

+1 我可以试试这个...我的简单“睡眠(1000)”现在似乎工作正常。【参考方案2】:

这里发生的很可能是FileCreated 事件正在引发并尝试在文件完全写入磁盘之前处理该文件。

请参阅Wait Until File Is Completely Written 了解避免此问题的技术。

【讨论】:

+1。但是知道为什么它适用于创建的第一个文件,而不适用于之后的其他文件吗? 不确定...答案将取决于用于创建文件的机制。【参考方案3】:

尝试禁用任何防病毒/反恶意软件。大多数配置为默认在创建时扫描文件。

【讨论】:

很好,但即使有例外规则(在生产环境中很少选择禁用),如果您在文件关闭后立即访问文件,我已经看到这些锁定。 也许,但它仍然没有解释为什么它工作一次,然后失败。 +1 无论如何都是一个有趣的想法。 (他们也不喜欢我们在工作时关闭杀毒软件:P) 近 8 年后,一位同事提到,他们认为这也是对所有新创建的文件执行的操作。没有办法证明它,也可能没有办法避免它。但只是想让你大声喊叫。 感谢您对该主题的投入 ;-) 如果您仍在为此苦苦挣扎,请检查是否有任何东西正在复制文件系统,或者底层存储是否有网络支持【参考方案4】:

Ty to EventHorizo​​n 获取上面的代码,非常适合我的需求,并且似乎抓住了 Brendan 分享的链接中的精髓。

将其稍微修改为返回元组的异步(既用于在 do/while 结束时取回文件锁定状态,也用于让 ms 传递以查看发生了哪种退出)。唯一让我不舒服的事情,我不确定为什么会这样,即使将延迟降低到 2ms 我仍然无法触发 catch IOException 条件(多个循环的证据) 所以我对 IO 故障案例没有任何直接可见性,其他人可能有更大的文件,他们可以通过以下方式验证这一点:

public async Task<(bool, int)> IsFileClosed(string filepath)

  bool fileClosed = false;
  int baseretries = 40;
  int retries = 40;
  const int delay = 250;

  if (!File.Exists(filepath))
    return (false,0);

  Task<bool> FileCheck = Task.Run(() =>
  
    do
    
      try
      
        FileStream fs = File.Open(filepath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
        fs.Close();
        fileClosed = true;
      
      catch (IOException)  
      retries--;

      if (!fileClosed)
        Thread.Sleep(delay);
    
    while (!fileClosed && retries > 0);
    return fileClosed;
  );

  fileClosed = await FileCheck;
  return (fileClosed, (baseretries - retries) * delay);

【讨论】:

以上是关于C# 中的 FileStream 和 FileSystemWatcher,奇怪的问题“进程无法访问文件”的主要内容,如果未能解决你的问题,请参考以下文章

C# 之 FileStream类介绍

C# 之 FileStream类介绍

c# FileStream函数的FileMode参数中的其中一个枚举值OpenOrCreate的说明是啥意思?

C#中FileStream和StreamWriter/StreamReader的区别

C# 计算输入和输出 FileStream 的 MD5

使用 c# 将 FileStream 编码为 base64