有没有办法检查文件是否正在使用?

我正在用 C#编写一个程序,该程序需要重复访问 1 个图像文件。在大多数情况下,它可以正常工作,但是如果我的计算机运行速度很快,它将在尝试将文件保存回文件系统之前尝试访问该文件,并抛出错误: “文件正在被另一个进程使用”

我想找到一种解决方法,但是我所有的 Google 搜索都只能通过使用异常处理来创建检查。这与我的宗教信仰背道而驰,所以我想知道是否有人能做得更好?

答案

您可能因此而遭受线程竞争的情况,有记录在此的示例被用作安全漏洞。如果您检查文件是否可用,然后尝试使用它,则可能会丢掉该文件,恶意用户可能会使用该文件来强制和利用您的代码。

最好的选择是 try catch / finally 尝试获取文件句柄。

try
{
   using (Stream stream = new FileStream("MyFilename.txt", FileMode.Open))
   {
        // File/Stream manipulating code here
   }
} catch {
  //check here why it failed and ask user to retry if the file is in use.
}

关于此解决方案的更新的 NOTE :使用FileAccess.ReadWrite进行的只读文件检查将失败,因此已修改解决方案以使用FileAccess.Read进行检查。尽管此解决方案有效,因为如果文件具有写入或读取锁定,则尝试使用FileAccess.Read进行检查将失败,但是,如果文件没有写入或读取锁定,则此解决方案将不起作用,即已通过 FileShare.Read 或 FileShare.Write 访问打开(用于读取或写入)。

原文:我在过去的几年中一直使用此代码,但至今没有任何问题。

了解您对使用异常的犹豫,但是您无法始终避免使用它们:

protected virtual bool IsFileLocked(FileInfo file)
{
    try
    {
        using(FileStream stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.None))
        {
            stream.Close();
        }
    }
    catch (IOException)
    {
        //the file is unavailable because it is:
        //still being written to
        //or being processed by another thread
        //or does not exist (has already been processed)
        return true;
    }

    //file is not locked
    return false;
}

使用此命令检查文件是否被锁定:

using System.IO;
using System.Runtime.InteropServices;
internal static class Helper
{
const int ERROR_SHARING_VIOLATION = 32;
const int ERROR_LOCK_VIOLATION = 33;

private static bool IsFileLocked(Exception exception)
{
    int errorCode = Marshal.GetHRForException(exception) & ((1 << 16) - 1);
    return errorCode == ERROR_SHARING_VIOLATION || errorCode == ERROR_LOCK_VIOLATION;
}

internal static bool CanReadFile(string filePath)
{
    //Try-Catch so we dont crash the program and can check the exception
    try {
        //The "using" is important because FileStream implements IDisposable and
        //"using" will avoid a heap exhaustion situation when too many handles  
        //are left undisposed.
        using (FileStream fileStream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) {
            if (fileStream != null) fileStream.Close();  //This line is me being overly cautious, fileStream will never be null unless an exception occurs... and I know the "using" does it but its helpful to be explicit - especially when we encounter errors - at least for me anyway!
        }
    }
    catch (IOException ex) {
        //THE FUNKY MAGIC - TO SEE IF THIS FILE REALLY IS LOCKED!!!
        if (IsFileLocked(ex)) {
            // do something, eg File.Copy or present the user with a MsgBox - I do not recommend Killing the process that is locking the file
            return false;
        }
    }
    finally
    { }
    return true;
}
}

出于性能原因,我建议您以相同的操作读取文件内容。这里有些例子:

public static byte[] ReadFileBytes(string filePath)
{
    byte[] buffer = null;
    try
    {
        using (FileStream fileStream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
        {
            int length = (int)fileStream.Length;  // get file length
            buffer = new byte[length];            // create buffer
            int count;                            // actual number of bytes read
            int sum = 0;                          // total number of bytes read

            // read until Read method returns 0 (end of the stream has been reached)
            while ((count = fileStream.Read(buffer, sum, length - sum)) > 0)
                sum += count;  // sum is a buffer offset for next reading

            fileStream.Close(); //This is not needed, just me being paranoid and explicitly releasing resources ASAP
        }
    }
    catch (IOException ex)
    {
        //THE FUNKY MAGIC - TO SEE IF THIS FILE REALLY IS LOCKED!!!
        if (IsFileLocked(ex))
        {
            // do something? 
        }
    }
    catch (Exception ex)
    {
    }
    finally
    {
    }
    return buffer;
}

public static string ReadFileTextWithEncoding(string filePath)
{
    string fileContents = string.Empty;
    byte[] buffer;
    try
    {
        using (FileStream fileStream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
        {
            int length = (int)fileStream.Length;  // get file length
            buffer = new byte[length];            // create buffer
            int count;                            // actual number of bytes read
            int sum = 0;                          // total number of bytes read

            // read until Read method returns 0 (end of the stream has been reached)
            while ((count = fileStream.Read(buffer, sum, length - sum)) > 0)
            {
                sum += count;  // sum is a buffer offset for next reading
            }

            fileStream.Close(); //Again - this is not needed, just me being paranoid and explicitly releasing resources ASAP

            //Depending on the encoding you wish to use - I'll leave that up to you
            fileContents = System.Text.Encoding.Default.GetString(buffer);
        }
    }
    catch (IOException ex)
    {
        //THE FUNKY MAGIC - TO SEE IF THIS FILE REALLY IS LOCKED!!!
        if (IsFileLocked(ex))
        {
            // do something? 
        }
    }
    catch (Exception ex)
    {
    }
    finally
    { }     
    return fileContents;
}

public static string ReadFileTextNoEncoding(string filePath)
{
    string fileContents = string.Empty;
    byte[] buffer;
    try
    {
        using (FileStream fileStream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
        {
            int length = (int)fileStream.Length;  // get file length
            buffer = new byte[length];            // create buffer
            int count;                            // actual number of bytes read
            int sum = 0;                          // total number of bytes read

            // read until Read method returns 0 (end of the stream has been reached)
            while ((count = fileStream.Read(buffer, sum, length - sum)) > 0) 
            {
                sum += count;  // sum is a buffer offset for next reading
            }

            fileStream.Close(); //Again - this is not needed, just me being paranoid and explicitly releasing resources ASAP

            char[] chars = new char[buffer.Length / sizeof(char) + 1];
            System.Buffer.BlockCopy(buffer, 0, chars, 0, buffer.Length);
            fileContents = new string(chars);
        }
    }
    catch (IOException ex)
    {
        //THE FUNKY MAGIC - TO SEE IF THIS FILE REALLY IS LOCKED!!!
        if (IsFileLocked(ex))
        {
            // do something? 
        }
    }
    catch (Exception ex)
    {
    }
    finally
    {
    }

    return fileContents;
}

自己尝试一下:

byte[] output1 = Helper.ReadFileBytes(@"c:\temp\test.txt");
string output2 = Helper.ReadFileTextWithEncoding(@"c:\temp\test.txt");
string output3 = Helper.ReadFileTextNoEncoding(@"c:\temp\test.txt");

也许您可以使用FileSystemWatcher 来监视 Changed 事件。

我自己没有使用过,但是值得一试。如果在这种情况下 filesystemwatcher 有点沉重,那么我将进行 try / catch / sleep 循环。

只需按预期使用异常即可。接受该文件正在使用中,然后重试,直到操作完成。这也是最有效的,因为您在执行操作之前不会浪费任何时间检查状态。

例如,使用以下功能

TimeoutFileAction(() => { System.IO.File.etc...; return null; } );

可重用的方法在 2 秒后超时

private T TimeoutFileAction<T>(Func<T> func)
{
    var started = DateTime.UtcNow;
    while ((DateTime.UtcNow - started).TotalMilliseconds < 2000)
    {
        try
        {
            return func();                    
        }
        catch (System.IO.IOException exception)
        {
            //ignore, or log somewhere if you want to
        }
    }
    return default(T);
}

您可以返回一个任务,该任务将在流可用时立即为您提供。这是一个简化的解决方案,但这是一个很好的起点。这是线程安全的。

private async Task<Stream> GetStreamAsync()
{
    try
    {
        return new FileStream("sample.mp3", FileMode.Open, FileAccess.Write);
    }
    catch (IOException)
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        return await GetStreamAsync();
    }
}

您可以照常使用此流:

using (var stream = await FileStreamGetter.GetStreamAsync())
{
    Console.WriteLine(stream.Length);
}

我知道的唯一方法是使用不太快的 Win32 独占锁 API,但是存在示例。

为了解决这个问题,大多数人只是简单地尝试 / 捕获 / 睡眠循环。

static bool FileInUse(string path)
    {
        try
        {
            using (FileStream fs = new FileStream(path, FileMode.OpenOrCreate))
            {
                fs.CanWrite
            }
            return false;
        }
        catch (IOException ex)
        {
            return true;
        }
    }

string filePath = "C:\\Documents And Settings\\yourfilename";
bool isFileInUse;

isFileInUse = FileInUse(filePath);

// Then you can do some checking
if (isFileInUse)
   Console.WriteLine("File is in use");
else
   Console.WriteLine("File is not in use");

希望这可以帮助!

上面接受的答案会遇到以下问题:如果已打开文件以 FileShare.Read 模式写入文件,或者该文件具有 “只读” 属性,则代码将不起作用。修改后的解决方案最可靠地工作,需要牢记两点(对于公认的解决方案也是如此):

  1. 它不适用于以写共享模式打开的文件
  2. 这没有考虑线程问题,因此您需要将其锁定或单独处理线程问题。

牢记以上几点,这将检查文件是被锁定以进行写入还是被锁定以防止读取

public static bool FileLocked(string FileName)
{
    FileStream fs = null;

    try
    {
        // NOTE: This doesn't handle situations where file is opened for writing by another process but put into write shared mode, it will not throw an exception and won't show it as write locked
        fs = File.Open(FileName, FileMode.Open, FileAccess.ReadWrite, FileShare.None); // If we can't open file for reading and writing then it's locked by another process for writing
    }
    catch (UnauthorizedAccessException) // https://msdn.microsoft.com/en-us/library/y973b725(v=vs.110).aspx
    {
        // This is because the file is Read-Only and we tried to open in ReadWrite mode, now try to open in Read only mode
        try
        {
            fs = File.Open(FileName, FileMode.Open, FileAccess.Read, FileShare.None);
        }
        catch (Exception)
        {
            return true; // This file has been locked, we can't even open it to read
        }
    }
    catch (Exception)
    {
        return true; // This file has been locked
    }
    finally
    {
        if (fs != null)
            fs.Close();
    }
    return false;
}

除了可以使用的 3 层衬纸,仅供参考:如果您需要完整的信息 - Microsoft Dev Center 上有一个小项目:

https://code.msdn.microsoft.com/windowsapps/How-to-know-the-process-704839f4

从简介:

在. NET Framework 4.0 中开发的 C#示例代码将有助于找出哪个进程锁定了文件。 rstrtmgr.dll中包含的RmStartSession函数已用于创建重新启动管理器会话,并根据返回结果创建 Win32Exception 对象的新实例。通过RmRegisterRescources函数将资源注册到 Restart Manager 会话后,通过枚举RM_PROCESS_INFO数组,调用RmGetList函数以检查哪些应用程序正在使用特定文件。

通过连接到 “重新启动管理器会话” 来工作。

重新启动管理器使用在会话中注册的资源列表来确定必须关闭并重新启动哪些应用程序和服务。 可以通过描述运行中的应用程序的文件名,服务简称或 RM_UNIQUE_PROCESS 结构来标识资源

对于您的特定需求,可能需要一些过度的工程... 但是,如果要这样做,请继续进行 vs 项目。