What's the right pattern for waiting on a file lock to be released?
Asked Answered
S

4

7

I need to open a file but if it's currently not available I need to wait until it's ready. What's the best approach to take?

SCENARIO

I'm using files as a persistent caching mechanism for application data. This data needs to be read and deserialized often (written only once, and deleted occasionally). I have a cleanup process that runs on a separate thread that determines which files are no longer needed and deletes them. Opening and reading of files may happen concurrently (rarely, but could happen) and I want the process to wait and try to read the data again.

Thanks!

Selmore answered 27/9, 2010 at 22:49 Comment(2)
Is this all taking place within a single process?Sculptor
yes. It's actually being used on a Windows 7 Phone (Silverlight).Selmore
S
9

I'm not a huge fan of the try/catch IOException because:

  1. The reason for the exception is unknown.
  2. I dislike 'expected' exceptions as I often run with break on exception.

You can do this without exceptions by calling CreateFile and returning a stream when/if it finally returns a handle:

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
Sculptor answered 27/9, 2010 at 23:45 Comment(0)
I
4

The more generic version of the csharptest.net's method could look like this (also, used SafeFileHandle and removed exception throwing on timeout, you can get enum values at 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);
Ithaman answered 8/9, 2013 at 9:5 Comment(0)
A
2

It depends on who controls the file. If one part of your application needs to wait until another part of the application finishes preparing the file, then you can use a ManualResetEvent. That is, at startup, your program creates a new event:

public ManualResetEvent FileEvent = new ManualResetEvent(false);

Now, the part of the program that's waiting for the file has this code:

FileEvent.WaitOne();

And the part of the program that's creating the file does this when the file's ready:

FileEvent.Set();

If your application has to wait for a file that is being used by another application that you have no control over, your only real solution is to continually try to open the file.

FileStream f = null;
while (f == null)
{
    try
    {
        f = new FileStream(...);
    }
    catch (IOException)
    {
        // wait a bit and try again
        Thread.Sleep(5000);
    }
}

Of course, you probably wouldn't want to unconditionally catch IOException. You'd likely want to catch the specific exceptions that you know how to handle (for example, you wouldn't want to try again if you got a DirectoryNotFoundException). The I/O functions document which exceptions they're expected to throw, and under what circumstances.

Ammoniacal answered 27/9, 2010 at 23:7 Comment(0)
W
0

Like all "what is the best approach" questions, this one depends on your needs. Some options that come easily to mind:

  1. Abort the attempt
  2. Loop until the file becomes unlocked
  3. Ask the user what to do about it

Which one you chose depends on how you can deal with it.

Wallacewallach answered 27/9, 2010 at 22:55 Comment(2)
1) is not a very effective means of waiting for the lock to become available. 3) is not a programmatic solution, though perhaps the correct one. And 2) is, I suspect, the default solution upon which Micah is trying to improve.Perpetual
I would point out that the original question looked nothing like its present form.Wallacewallach

© 2022 - 2024 — McMap. All rights reserved.