等待,直到文件在.NET 中解锁

在文件解锁并可以读取和重命名之前,阻塞线程的最简单方法是什么?例如,在。NET 框架? ?

我有一个使用 FileSystemWatcher 查找要传输到 FTP 站点的文件的服务,但是在其他进程完成写入文件之前,文件已创建事件触发。

理想的解决方案应该有一个超时期,这样线程在放弃之前就不会永远挂起。

编辑: 在尝试了下面的一些解决方案之后,我最终更改了 系统,以便所有文件都写入 Path.GetTempFileName(),然后执行 File.Move()到最终位置。FileSystemWatcher事件一触发,文件就已经完成。

90358 次浏览

一段时间以前,我使用的一种技术是编写自己的函数。基本上捕获异常并使用可以在指定时间内触发的计时器重试。如果有更好的方法,请与我们分享。

我不知道你用什么来确定文件的锁定状态,但这样的东西应该可以做到这一点。

while (true)
{
try {
stream = File.Open( fileName, fileMode );
break;
}
catch( FileIOException ) {


// check whether it's a lock problem


Thread.Sleep( 100 );
}
}

From MSDN:

The OnCreated event is raised as soon 如果文件是 被复制或转移到 被监视的目录,OnCreated 事件 将立即提高,随后 由一个或多个 OnChanged 事件。

可以修改 FileSystemWatcher,使其在“ OnCreated”事件期间不执行读/重命名,而是:

  1. 使用 FileInfo 对象查询文件状态,直到文件状态未被锁定为止
  2. Calls back into the service to process the file as soon as it determines the file is no longer locked and is ready to go

这是我在 related question上给出的答案:

    /// <summary>
/// Blocks until the file is not locked any more.
/// </summary>
/// <param name="fullPath"></param>
bool WaitForFile(string fullPath)
{
int numTries = 0;
while (true)
{
++numTries;
try
{
// Attempt to open the file exclusively.
using (FileStream fs = new FileStream(fullPath,
FileMode.Open, FileAccess.ReadWrite,
FileShare.None, 100))
{
fs.ReadByte();


// If we got this far the file is ready
break;
}
}
catch (Exception ex)
{
Log.LogWarning(
"WaitForFile {0} failed to get an exclusive lock: {1}",
fullPath, ex.ToString());


if (numTries > 10)
{
Log.LogWarning(
"WaitForFile {0} giving up after 10 tries",
fullPath);
return false;
}


// Wait for the lock to be released
System.Threading.Thread.Sleep(500);
}
}


Log.LogTrace("WaitForFile {0} returning true after {1} tries",
fullPath, numTries);
return true;
}

我和古尔扎一样,只要不断循环就行了。

事实上,我甚至不用担心文件系统监视器。每分钟轮询一次网络驱动器以获取新文件是便宜的。

在大多数情况下,像@harpo 这样的简单方法是可行的,你可以使用这种方法开发更复杂的代码:

  • 使用 SystemHandleInformationSystemProcessInformation 查找选定文件的所有打开句柄
  • 子类 WaitHandle 获得对其内部句柄的访问权限
  • 将包装在子类 WaitHandle 中的找到句柄传递给 WaitHandle.WaitAnymethod

对于这个特定的应用程序,直接观察文件将不可避免地导致难以跟踪的错误,特别是当文件大小增加时。这里有两种不同的策略可以奏效。

  • FTP 两个文件,但只看一个。例如,发送 impant.txt 和 impant.Finish 文件。只观察完成文件,但处理文本。
  • FTP 一个文件,但完成后重命名它。例如,发送 impant.wait,并让发送方在完成后将其重命名为 impant.txt。

祝你好运!

不如这样吧:

private void WaitOnFile(string fileName)
{
FileInfo fileInfo = new FileInfo(fileName);
for (long size = -1; size != fileInfo.Length; fileInfo.Refresh())
{
size = fileInfo.Length;
System.Threading.Thread.Sleep(1000);
}
}

当然,如果在创建时预先分配了文件大小,就会得到一个假阳性。

我为这类事情组织了一个帮助类。它将工作,如果你有一切的控制,将访问该文件。如果您期望从一大堆其他事情中获得争用,那么这是非常没有价值的。

using System;
using System.IO;
using System.Threading;


/// <summary>
/// This is a wrapper aroung a FileStream.  While it is not a Stream itself, it can be cast to
/// one (keep in mind that this might throw an exception).
/// </summary>
public class SafeFileStream: IDisposable
{
#region Private Members
private Mutex m_mutex;
private Stream m_stream;
private string m_path;
private FileMode m_fileMode;
private FileAccess m_fileAccess;
private FileShare m_fileShare;
#endregion//Private Members


#region Constructors
public SafeFileStream(string path, FileMode mode, FileAccess access, FileShare share)
{
m_mutex = new Mutex(false, String.Format("Global\\{0}", path.Replace('\\', '/')));
m_path = path;
m_fileMode = mode;
m_fileAccess = access;
m_fileShare = share;
}
#endregion//Constructors


#region Properties
public Stream UnderlyingStream
{
get
{
if (!IsOpen)
throw new InvalidOperationException("The underlying stream does not exist - try opening this stream.");
return m_stream;
}
}


public bool IsOpen
{
get { return m_stream != null; }
}
#endregion//Properties


#region Functions
/// <summary>
/// Opens the stream when it is not locked.  If the file is locked, then
/// </summary>
public void Open()
{
if (m_stream != null)
throw new InvalidOperationException(SafeFileResources.FileOpenExceptionMessage);
m_mutex.WaitOne();
m_stream = File.Open(m_path, m_fileMode, m_fileAccess, m_fileShare);
}


public bool TryOpen(TimeSpan span)
{
if (m_stream != null)
throw new InvalidOperationException(SafeFileResources.FileOpenExceptionMessage);
if (m_mutex.WaitOne(span))
{
m_stream = File.Open(m_path, m_fileMode, m_fileAccess, m_fileShare);
return true;
}
else
return false;
}


public void Close()
{
if (m_stream != null)
{
m_stream.Close();
m_stream = null;
m_mutex.ReleaseMutex();
}
}


public void Dispose()
{
Close();
GC.SuppressFinalize(this);
}


public static explicit operator Stream(SafeFileStream sfs)
{
return sfs.UnderlyingStream;
}
#endregion//Functions
}

它使用一个命名的互斥对象工作。那些希望访问该文件的用户试图获得对命名互斥对象的控制权,该互斥对象共享该文件的名称(“的”变成“/的”)。您可以使用 Open () ,它会一直延迟到互斥对象可以访问为止; 或者您可以使用 TryOpen (TimeSpan) ,它会尝试在给定的持续时间内获取互斥对象,如果在时间跨度内无法获取,则返回 false。这很可能是在使用块中使用的,以确保正确释放锁,并且在释放此对象时,流(如果打开)将被正确释放。

我做了一个包含约20项内容的快速测试,对文件执行各种读/写操作,没有发现任何损坏。显然它不是非常先进,但它应该为大多数简单的情况下工作。

从 Eric 的回答开始,我做了一些改进,使代码更加紧凑和可重用。

FileStream WaitForFile (string fullPath, FileMode mode, FileAccess access, FileShare share)
{
for (int numTries = 0; numTries < 10; numTries++) {
FileStream fs = null;
try {
fs = new FileStream (fullPath, mode, access, share);
return fs;
}
catch (IOException) {
if (fs != null) {
fs.Dispose ();
}
Thread.Sleep (50);
}
}


return null;
}

下面是执行此操作的通用代码,独立于文件操作本身。这是一个如何使用它的例子:

WrapSharingViolations(() => File.Delete(myFile));

或者

WrapSharingViolations(() => File.Copy(mySourceFile, myDestFile));

还可以定义重试次数和重试之间的等待时间。

注意: 遗憾的是,基础 Win32错误(ERROR _ SHARING _ VIOLATION)没有公开。NET,所以我添加了一个基于反射机制的小型 hack 函数(IsSharingViolation)来检查这一点。

    /// <summary>
/// Wraps sharing violations that could occur on a file IO operation.
/// </summary>
/// <param name="action">The action to execute. May not be null.</param>
public static void WrapSharingViolations(WrapSharingViolationsCallback action)
{
WrapSharingViolations(action, null, 10, 100);
}


/// <summary>
/// Wraps sharing violations that could occur on a file IO operation.
/// </summary>
/// <param name="action">The action to execute. May not be null.</param>
/// <param name="exceptionsCallback">The exceptions callback. May be null.</param>
/// <param name="retryCount">The retry count.</param>
/// <param name="waitTime">The wait time in milliseconds.</param>
public static void WrapSharingViolations(WrapSharingViolationsCallback action, WrapSharingViolationsExceptionsCallback exceptionsCallback, int retryCount, int waitTime)
{
if (action == null)
throw new ArgumentNullException("action");


for (int i = 0; i < retryCount; i++)
{
try
{
action();
return;
}
catch (IOException ioe)
{
if ((IsSharingViolation(ioe)) && (i < (retryCount - 1)))
{
bool wait = true;
if (exceptionsCallback != null)
{
wait = exceptionsCallback(ioe, i, retryCount, waitTime);
}
if (wait)
{
System.Threading.Thread.Sleep(waitTime);
}
}
else
{
throw;
}
}
}
}


/// <summary>
/// Defines a sharing violation wrapper delegate.
/// </summary>
public delegate void WrapSharingViolationsCallback();


/// <summary>
/// Defines a sharing violation wrapper delegate for handling exception.
/// </summary>
public delegate bool WrapSharingViolationsExceptionsCallback(IOException ioe, int retry, int retryCount, int waitTime);


/// <summary>
/// Determines whether the specified exception is a sharing violation exception.
/// </summary>
/// <param name="exception">The exception. May not be null.</param>
/// <returns>
///     <c>true</c> if the specified exception is a sharing violation exception; otherwise, <c>false</c>.
/// </returns>
public static bool IsSharingViolation(IOException exception)
{
if (exception == null)
throw new ArgumentNullException("exception");


int hr = GetHResult(exception, 0);
return (hr == -2147024864); // 0x80070020 ERROR_SHARING_VIOLATION


}


/// <summary>
/// Gets the HRESULT of the specified exception.
/// </summary>
/// <param name="exception">The exception to test. May not be null.</param>
/// <param name="defaultValue">The default value in case of an error.</param>
/// <returns>The HRESULT value.</returns>
public static int GetHResult(IOException exception, int defaultValue)
{
if (exception == null)
throw new ArgumentNullException("exception");


try
{
const string name = "HResult";
PropertyInfo pi = exception.GetType().GetProperty(name, BindingFlags.NonPublic | BindingFlags.Instance); // CLR2
if (pi == null)
{
pi = exception.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance); // CLR4
}
if (pi != null)
return (int)pi.GetValue(exception, null);
}
catch
{
}
return defaultValue;
}

传输进程触发器文件 SameNameASTrasferedFile.trg 的广告 文件传输完成后创建的。

然后设置 FileSystemWatcher,它只在 * . trg 文件上触发事件。

只需将 变了事件与 NotifyFilter NotifyFilters. LastWrite一起使用:

var watcher = new FileSystemWatcher {
Path = @"c:\temp\test",
Filter = "*.xml",
NotifyFilter = NotifyFilters.LastWrite
};
watcher.Changed += watcher_Changed;
watcher.EnableRaisingEvents = true;

我在添加 Outlook 附件时遇到了类似的问题。

string fileName = MessagingBLL.BuildPropertyAttachmentFileName(currProp);


//create a temporary file to send as the attachment
string pathString = Path.Combine(Path.GetTempPath(), fileName);


//dirty trick to make sure locks are released on the file.
using (System.IO.File.Create(pathString)) { }


mailItem.Subject = MessagingBLL.PropertyAttachmentSubject;
mailItem.Attachments.Add(pathString, Outlook.OlAttachmentType.olByValue, Type.Missing, Type.Missing);

一个可能的解决方案是,将文件系统监视器与一些轮询相结合,

获得通知的每一个文件上的变化,当得到通知检查是否是 锁定,如目前接受的答案中所述: https://stackoverflow.com/a/50800/6754146 打开文件流的代码是从答案中复制并略加修改的:

public static void CheckFileLock(string directory, string filename, Func<Task> callBack)
{
var watcher = new FileSystemWatcher(directory, filename);
FileSystemEventHandler check =
async (sender, eArgs) =>
{
string fullPath = Path.Combine(directory, filename);
try
{
// Attempt to open the file exclusively.
using (FileStream fs = new FileStream(fullPath,
FileMode.Open, FileAccess.ReadWrite,
FileShare.None, 100))
{
fs.ReadByte();
watcher.EnableRaisingEvents = false;
// If we got this far the file is ready
}
watcher.Dispose();
await callBack();
}
catch (IOException) { }
};
watcher.NotifyFilter = NotifyFilters.LastWrite;
watcher.IncludeSubdirectories = false;
watcher.EnableRaisingEvents = true;
//Attach the checking to the changed method,
//on every change it gets checked once
watcher.Changed += check;
//Initially do a check for the case it is already released
check(null, null);
}

通过这种方式,您可以检查一个文件是否被锁定,并在指定的回调函数上关闭时得到通知,这样您就避免了过度激进的轮询,并且只在它可能实际被关闭时才进行工作

除了我添加了一个检查以查看文件是否存在之外,以下是一个类似的答案。

bool WaitForFile(string fullPath)
{
int numTries = 0;
while (true)
{
//need to add this line to prevent infinite loop
if (!File.Exists(fullPath))
{
_logger.LogInformation("WaitForFile {0} returning true - file does not exist", fullPath);
break;
}
++numTries;
try
{
// Attempt to open the file exclusively.
using (FileStream fs = new FileStream(fullPath,
FileMode.Open, FileAccess.ReadWrite,
FileShare.None, 100))
{
fs.ReadByte();


// If we got this far the file is ready
break;
}
}
catch (Exception ex)
{
_logger.LogInformation(
"WaitForFile {0} failed to get an exclusive lock: {1}",
fullPath, ex.ToString());


if (numTries > 10)
{
_logger.LogInformation(
"WaitForFile {0} giving up after 10 tries",
fullPath);
return false;
}


// Wait for the lock to be released
System.Threading.Thread.Sleep(500);
}
}


_logger.LogInformation("WaitForFile {0} returning true after {1} tries",
fullPath, numTries);
return true;
}