等待文件锁释放的正确模式是什么?

6

我需要打开一个文件,但如果该文件当前不可用,我需要等待,直到它准备好。采取什么最好的方法?

场景

我正在使用文件作为应用程序数据的持久缓存机制。这些数据需要经常读取和反序列化(只写入一次,并偶尔删除)。我有一个清理进程在单独的线程上运行,确定哪些文件不再需要并将它们删除。文件的打开和读取可能会同时发生(很少,但可能发生),我希望该过程等待并尝试重新读取数据。

谢谢!


这是否全部发生在单个进程中? - csharptest.net
是的。它实际上正在Windows 7手机(Silverlight)上使用。 - Micah
4个回答

9

我不是try/catch IOException的忠实拥护者,因为:

  1. 异常的原因未知。
  2. 我不喜欢“预期”的异常,因为我经常在异常上使用断点。

您可以通过调用CreateFile并在最终返回句柄时返回流来避免使用异常:

public static System.IO.Stream WaitForExclusiveFileAccess(string filePath, int timeout)
{
    IntPtr fHandle;
    int errorCode;
    DateTime start = DateTime.Now;

    while(true)
    {
        fHandle = CreateFile(filePath, EFileAccess.GenericRead | EFileAccess.GenericWrite, EFileShare.None, IntPtr.Zero,
                             ECreationDisposition.OpenExisting, EFileAttributes.Normal, IntPtr.Zero);

        if (fHandle != IntPtr.Zero && fHandle.ToInt64() != -1L)
            return new System.IO.FileStream(fHandle, System.IO.FileAccess.ReadWrite, true);

        errorCode = Marshal.GetLastWin32Error();

        if (errorCode != ERROR_SHARING_VIOLATION)
            break;
        if (timeout >= 0 && (DateTime.Now - start).TotalMilliseconds > timeout)
            break;
        System.Threading.Thread.Sleep(100);
    }


    throw new System.IO.IOException(new System.ComponentModel.Win32Exception(errorCode).Message, errorCode);
}

#region Win32
const int ERROR_SHARING_VIOLATION = 32;

[Flags]
enum EFileAccess : uint
{
    GenericRead = 0x80000000,
    GenericWrite = 0x40000000
}

[Flags]
enum EFileShare : uint
{
    None = 0x00000000,
}

enum ECreationDisposition : uint
{
    OpenExisting = 3,
}

[Flags]
enum EFileAttributes : uint
{
    Normal = 0x00000080,
}

[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
static extern IntPtr CreateFile(
   string lpFileName,
   EFileAccess dwDesiredAccess,
   EFileShare dwShareMode,
   IntPtr lpSecurityAttributes,
   ECreationDisposition dwCreationDisposition,
   EFileAttributes dwFlagsAndAttributes,
   IntPtr hTemplateFile);

#endregion

4
更通用的csharptest.net方法的版本可能看起来像这样(同时使用了SafeFileHandle并移除了超时异常抛出,您可以在http://www.pinvoke.net/default.aspx/kernel32.createfile获取枚举值):
    public static FileStream WaitForFileAccess(string filePath, FileMode fileMode, FileAccess access, FileShare share, TimeSpan timeout)
    {
        int errorCode;
        DateTime start = DateTime.Now;

        while (true)
        {
            SafeFileHandle fileHandle = CreateFile(filePath, ConvertFileAccess(access), ConvertFileShare(share), IntPtr.Zero,
                                                   ConvertFileMode(fileMode), EFileAttributes.Normal, IntPtr.Zero);

            if (!fileHandle.IsInvalid)
            {
                return new FileStream(fileHandle, access);
            }

            errorCode = Marshal.GetLastWin32Error();

            if (errorCode != ERROR_SHARING_VIOLATION)
            {
                break;
            }

            if ((DateTime.Now - start) > timeout)
            {
                return null; // timeout isn't an exception
            }

            Thread.Sleep(100);
        }

        throw new IOException(new Win32Exception(errorCode).Message, errorCode);
    }

    private static EFileAccess ConvertFileAccess(FileAccess access)
    {
        return access == FileAccess.ReadWrite ? EFileAccess.GenericRead | EFileAccess.GenericWrite : access == FileAccess.Read ? EFileAccess.GenericRead : EFileAccess.GenericWrite;
    }

    private static EFileShare ConvertFileShare(FileShare share)
    {
        return (EFileShare) ((uint) share);
    }

    private static ECreationDisposition ConvertFileMode(FileMode mode)
    {
        return mode == FileMode.Open ? ECreationDisposition.OpenExisting : mode == FileMode.OpenOrCreate ? ECreationDisposition.OpenAlways : (ECreationDisposition) (uint) mode;
    }

    [DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern SafeFileHandle CreateFile(
       string lpFileName,
       EFileAccess dwDesiredAccess,
       EFileShare dwShareMode,
       IntPtr lpSecurityAttributes,
       ECreationDisposition dwCreationDisposition,
       EFileAttributes dwFlagsAndAttributes,
       IntPtr hTemplateFile);

2
这取决于文件的控制权。如果应用程序的一部分需要等待另一部分准备好文件,那么可以使用ManualResetEvent。也就是说,在启动时,程序创建一个新事件:
``` public ManualResetEvent FileEvent = new ManualResetEvent(false); ```
现在,等待文件的程序部分有下面这段代码:
``` FileEvent.WaitOne(); ```
而创建文件的程序部分在文件准备好后执行以下操作:
``` FileEvent.Set(); ```
如果您的应用程序必须等待被其他您无法控制的应用程序使用的文件,则唯一的解决方案是不断尝试打开该文件。
FileStream f = null;
while (f == null)
{
    try
    {
        f = new FileStream(...);
    }
    catch (IOException)
    {
        // wait a bit and try again
        Thread.Sleep(5000);
    }
}

当然,您可能不想无条件地捕获 IOException。您可能想要捕获您知道如何处理的具体异常(例如,如果您遇到 DirectoryNotFoundException,您可能不想再尝试)。I/O函数记录了它们预计抛出哪些异常以及在什么情况下抛出。


0

像所有“最佳方法是什么”的问题一样,这取决于您的需求。一些容易想到的选项:

  1. 中止尝试
  2. 循环直到文件解锁
  3. 询问用户如何处理

您选择哪个取决于您如何处理它。


1
  1. 等待锁变得可用的方式并不是非常有效。
  2. 不是编程解决方案,但也许是正确的解决方案。
  3. 我猜想,Micah 正试图改进的是默认解决方案。
- Kirk Woll
我想指出的是,原始问题与现在的形式完全不同。 - Tergiver

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接