112

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

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

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

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

4

16 回答 16

84

从 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;
}
于 2010-09-09T15:19:00.653 回答
45

这是我在一个相关问题上给出的答案:

    /// <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;
    }
于 2008-09-08T21:59:11.790 回答
19

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

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

或者

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

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

注意:不幸的是,底层的 Win32 错误 (ERROR_SHARING_VIOLATION) 没有在 .NET 中公开,所以我添加了一个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;
    }
于 2011-03-26T10:12:45.600 回答
15

我为这些事情拼凑了一个助手类。如果您可以控制将访问该文件的所有内容,它将起作用。如果您期望来自一堆其他事物的争用,那么这是毫无价值的。

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 件事做了一个快速测试,对文件进行各种读/写,没有发现损坏。显然它不是很先进,但它应该适用于大多数简单的情况。

于 2009-08-07T22:40:56.717 回答
5

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

  • Ftp 两个文件,但只看一个。例如发送文件important.txt 和important.finish。只注意完成文件,但处理 txt。
  • FTP 一个文件,但完成后重命名。例如发送 important.wait 并让发件人在完成后将其重命名为 important.txt。

祝你好运!

于 2008-09-09T00:20:33.183 回答
4

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

于 2008-09-08T21:37:55.727 回答
3

来自MSDN

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

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

  1. 生成一个轮询文件状态的线程,直到它未被锁定(使用 FileInfo 对象)
  2. 一旦确定文件不再锁定并准备就绪,就立即回调服务以处理文件
于 2008-09-08T21:48:11.973 回答
2

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

  • 使用 SystemHandleInformation\SystemProcessInformation 查找所选文件的所有打开句柄
  • 子类 WaitHandle 类以访问它的内部句柄
  • 将发现的句柄包裹在子类 WaitHandle 中传递给 WaitHandle.WaitAny 方法
于 2008-09-09T00:11:30.147 回答
2

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

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

于 2011-07-08T15:10:18.290 回答
1

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

而(真)
{
    尝试 {
        流 = File.Open(文件名,文件模式);
        休息;
    }
    捕捉(文件IOException){

        // 检查是否是锁问题

        线程.睡眠(100);
    }
}
于 2008-09-08T21:42:27.290 回答
1

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

为文件的每个更改获取通知,并在收到通知时检查它是否如当前接受的答案中所述被锁定: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);
}

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

于 2017-03-14T09:40:15.297 回答
0

这是与上述类似的答案,除了我添加了一个检查以查看文件是否存在。

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;
        }
于 2021-05-06T19:21:25.217 回答
-1

我的做法和 Gulzar 一样,只是不断尝试循环。

事实上,我什至不关心文件系统观察程序。每分钟轮询一次网络驱动器以获取新文件很便宜。

于 2008-09-08T23:14:58.567 回答
-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;
于 2013-01-22T20:51:29.733 回答
-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);
于 2014-03-04T16:58:04.827 回答
-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);
    }
}

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

于 2009-07-23T12:56:35.410 回答