等待文件在.NET中解锁


103

在文件被解锁并可以读取和重命名之前,阻塞线程的最简单方法是什么?例如,.NET Framework中是否有WaitOnFile()?

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

理想的解决方案将有一个超时期限,因此在放弃之前线程不会永远挂起。

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


4
从.NET 4.0版本开始,是否有更好的方法来解决此问题?
杰森

Answers:


40

这是我对一个相关问题的回答

    /// <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;
    }

7
我发现这个丑陋但唯一可行的解​​决方案
knoopx

6
在一般情况下,这真的有用吗?如果您在using()子句中打开文件,则在使用范围结束时文件将被关闭和解锁。如果第二个进程使用与此策略相同的策略(重复尝试),则在WaitForFile()退出后,将出现有关文件是否可打开的竞争条件。没有?
Cheeso

74
馊主意!虽然这个概念是正确的,但是更好的解决方案是返回FileStream而不是bool。如果文件在用户有机会获得对文件的锁定之前再次被锁定-即使该函数返回了“ false”,他也将获得异常
Nissim

2
费罗的方法在哪里?
Vbp

1
Nissim的评论也正是我在想的,但是如果您要使用该搜索,请不要忘记在读取字节后将其重置为0。 fs.Seek(0,SeekOrigin.Begin);
2015年

73

从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;
}

16
我从未来开始说,这段代码仍然可以发挥作用。谢谢。
OnoSendai

6
@PabloCosta正是!它无法关闭它,因为如果这样做,则另一个线程可能会参与竞争并打开它,从而无法达到目的。此实现是正确的,因为它可以保持打开状态!让调用者担心这一点,将它设为usingnull 是安全的,只需检查该using块内是否为null 。
doug65536 '16

2
“ FileStream fs = null;” 应该在try之外但在for内声明。然后在try中分配并使用fs。catch块应执行“ if(fs!= null)fs.Dispose();” (或仅在C#6中为fs?.Dispose())以确保正确清除了未返回的FileStream。
Bill Menees

1
真的有必要读取一个字节吗?以我的经验,如果您已打开该文件以进行读取访问,则可以拥有它,而不必对其进行测试。尽管使用此处的设计并不需要强制独占访问,所以甚至有可能您可以读取第一个字节,但不能读取其他字节(字节级别锁定)。从最初的问题来看,您可能会以只读共享级别打开,因此没有其他进程可以锁定或修改文件。无论如何,我觉得fs.ReadByte()要么完全浪费,要么还不够,这取决于使用情况。
eselk '17

8
用户哪种情况fs不能在catch区块中为空?如果FileStream构造函数抛出异常,则不会为变量分配值,并且变量中没有其他try可引发的值IOException。在我看来,这样做就可以了return new FileStream(...)
Matti Virkkunen

18

这是独立于文件操作本身的通用代码。这是有关如何使用它的示例:

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

要么

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

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

注意:不幸的是,.NET并未公开潜在的Win32错误(ERROR_SHARING_VIOLATION),因此我添加了一个IsSharingViolation基于反射机制的小型hack函数()来进行检查。

    /// <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;
    }

5
他们本来可以提供一个SharingViolationException。实际上,只要它从降下来,它们仍然可以向后兼容IOException。他们真的,真的应该。
罗曼·斯塔科夫


9
在.NET Framework 4.5,.NET Standard和.NET Core中,HResult是Exception类的公共属性。为此,不再需要反思。从MSDN:Starting with the .NET Framework 4.5, the HResult property's setter is protected, whereas its getter is public. In previous versions of the .NET Framework, both getter and setter are protected.
NightOwl888

13

我为这类事情召集了一个帮助班。如果您可以控制将访问该文件的所有内容,它将起作用。如果您期望其他方面的竞争,那么这毫无价值。

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)。这应该最有可能在using块内使用,以确保正确释放锁,并且在处置此对象时将正确处置流(如果已打开)。

我用大约20件事进行了快速测试,以对文件进行各种读/写操作,未发现损坏。显然,它不是很高级,但是它应该适用于大多数简单情况。


5

对于此特定应用程序,直接观察文件将不可避免地导致难以跟踪的错误,尤其是当文件大小增加时。这是两种可行的策略。

  • ftp两个文件,但只能观看一个。例如,发送文件Important.txt和Important.finish。仅注意整理文件,但处理txt。
  • FTP一个文件,但完成后将其重命名。例如,发送Important.wait,并在完成后让发送方将其重命名为Important.txt。

祝好运!


这与自动相反。这就像手动获取文件,需要更多步骤。
HackSlash

4

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


3

MSDN

创建文件后立即引发OnCreated事件。如果文件正在复制或转移到监视的目录中,则将立即引发OnCreated事件,然后是一个或多个OnChanged事件。

您可以修改FileSystemWatcher,以便在“ OnCreated”事件期间不进行读取/重命名,而是:

  1. 生成一个轮询文件状态的线程,直到它未被锁定为止(使用FileInfo对象)
  2. 一旦确定文件不再被锁定并且可以使用了,则回调服务以处理文件

1
生成filesystemwatcher的线程可能会导致基础缓冲区溢出,从而丢失大量已更改的文件。更好的方法是创建消费者/生产者队列。
Nissim

2

在大多数情况下,建议使用@harpo这样的简单方法。您可以使用这种方法开发更复杂的代码:

  • 使用SystemHandleInformation \ SystemProcessInformation查找所选文件的所有打开的句柄
  • 子类WaitHandle类可访问其内部句柄
  • 将包装在子类WaitHandle中的找到的句柄传递给WaitHandle.WaitAny方法

2

广告传输过程触发文件SameNameASTrasferedFile.trg,该文件在文件传输完成后创建。

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


1

我不知道您要使用什么来确定文件的锁定状态,但是应该这样做。

而(真)
{
    尝试{
        流= File.Open(fileName,fileMode);
        打破;
    }
    catch(FileIOException){

        //检查是否存在锁定问题

        Thread.Sleep(100);
    }
}

1
有点晚了,但是当文件以某种方式被锁定时,您将永远不会退出循环。您应该添加一个计数器(请参阅第一个答案)。
彼得

0

一个可能的解决方案是,将filesystemwatcher与一些轮询结合起来,

对于文件中的每个更改都得到通知,并在收到通知时检查是否按照当前接受的答案中的说明被锁定: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);
}

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


-1

我做的方式与Gulzar相同,只是不断尝试循环。

实际上,我什至不理会文件系统监视程序。每分钟轮询一次网络驱动器以获取新文件很便宜。


2
它可能很便宜,但对于许多应用程序来说,每分钟一次太长了。实时监控有时是必不可少的。您不必使用CSW(不是最方便的语言)来实现将监听C#文件系统消息的方法,而使用FSW。
ThunderGr

-1

只需将Changed事件与NotifyFilter NotifyFilters.LastWrite一起使用

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

1
FileSystemWatcher不仅会通知何时完成文件写入。它通常会多次通知您一次“单次”逻辑写入,并且如果您在收到第一个通知后尝试打开文件,则会收到异常。
罗斯,

-1

添加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);

-3

如何作为一个选择:

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);
    }
}

当然,如果文件大小在创建时已预先分配,您将得到误报。


1
如果写入文件的过程暂停一秒钟以上,或在内存中缓冲超过一秒钟,那么您将得到另一个误报。我认为这在任何情况下都不是一个好的解决方案。
克里斯·文纳姆
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.