问题
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!
回答1:
I'm not a huge fan of the try/catch IOException because:
- The reason for the exception is unknown.
- 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
回答2:
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);
回答3:
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.
回答4:
Like all "what is the best approach" questions, this one depends on your needs. Some options that come easily to mind:
- Abort the attempt
- Loop until the file becomes unlocked
- Ask the user what to do about it
Which one you chose depends on how you can deal with it.
来源:https://stackoverflow.com/questions/3808474/whats-the-right-pattern-for-waiting-on-a-file-lock-to-be-released