等到文件在 .NET 中解锁

Posted

技术标签:

【中文标题】等到文件在 .NET 中解锁【英文标题】:Wait until file is unlocked in .NET 【发布时间】:2010-09-08 05:20:02 【问题描述】:

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

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

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

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

【问题讨论】:

自.NET 4.0发布以来,有没有更好的办法来解决这个问题? 【参考方案1】:

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

【讨论】:

我来自未来说这段代码仍然像魅力一样工作。谢谢。 @PabloCosta 没错!它不能关闭它,因为如果它关闭了,另一个线程可能会争相进入并打开它,从而破坏目的。这个实现是正确的,因为它保持开放!让调用者担心,在 null 上使用 using 是安全的,只需检查 using 块内的 null。 "FileStream fs = null;"应该在 try 之外但在 for 中声明。然后在try里面分配和使用fs。 catch 块应该执行“if (fs != null) fs.Dispose();” (或者只是 C#6 中的 fs?.Dispose())以确保正确清理未返回的 FileStream。 真的有必要读一个字节吗?根据我的经验,如果您打开文件进行读取访问,您就拥有它,无需测试它。尽管这里的设计没有强制独占访问,所以甚至有可能您可以读取第一个字节,但不能读取其他字节(字节级锁定)。从最初的问题来看,您可能会以只读共享级别打开,因此没有其他进程可以锁定或修改文件。无论如何,我觉得 fs.ReadByte() 要么完全浪费,要么不够用,具体取决于使用情况。 用户在catch块中fs不为空的情况是什么?如果FileStream 构造函数抛出,变量将不会被赋值,并且try 内没有其他东西可以抛出IOException。对我来说,做return new FileStream(...)似乎应该没问题。【参考方案2】:

这是我在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;
    

【讨论】:

我觉得这很丑,但唯一可能的解决方案 这真的适用于一般情况吗?如果您在 using() 子句中打开文件,则在 using 范围结束时文件将关闭并解锁。如果有第二个进程使用与此相同的策略(重复重试),则在 WaitForFile() 退出后,存在关于文件是否可打开的竞争条件。没有? 坏主意!虽然这个概念是正确的,但更好的解决方案是返回 FileStream 而不是 bool。如果在用户有机会获得文件锁定之前文件再次被锁定 - 即使函数返回“false”,他也会收到异常 Fero的方法在哪里? Nissim 的评论也正是我的想法,但如果您要使用该搜索,请不要忘记在读取字节后将其重置为 0。 fs.Seek(0, SeekOrigin.Begin);【参考方案3】:

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

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;
    

【讨论】:

他们真的可以提供SharingViolationException。事实上,它们仍然可以向后兼容,只要它来自IOException。他们真的,真的应该。 Marshal.GetHRForException msdn.microsoft.com/en-us/library/… 在 .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.【参考方案4】:

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

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。

祝你好运!

【讨论】:

这与自动相反。这就像手动获取文件,需要更多步骤。【参考方案6】:

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

【讨论】:

【参考方案7】:

来自MSDN:

OnCreated 事件尽快引发 作为一个文件被创建。如果一个文件是 被复制或转移到 监视目录,OnCreated 事件 将立即提出,随后 通过一个或多个 OnChanged 事件。

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

    生成一个轮询文件状态的线程,直到它未被锁定(使用 FileInfo 对象) 一旦确定文件不再被锁定并准备就绪,就会回调服务以处理文件

【讨论】:

产生filesystemwatcher的线程会导致底层缓冲区溢出,从而丢失大量更改的文件。更好的方法是创建消费者/生产者队列。【参考方案8】:

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

使用 SystemHandleInformation\SystemProcessInformation 查找所选文件的所有打开句柄 子类 WaitHandle 类以访问其内部句柄 将发现的句柄传递给 WaitHandle.WaitAny 方法

【讨论】:

【参考方案9】:

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

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

【讨论】:

【参考方案10】:

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

而(真) 尝试 流 = File.Open(文件名,文件模式); 休息; 捕捉(文件IO异常) // 检查是否是锁问题 线程.睡眠(100);

【讨论】:

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

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

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

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

【讨论】:

【参考方案12】:

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

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;
        

【讨论】:

【参考方案13】:

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

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

【讨论】:

它可能很便宜,但对于很多应用程序来说,一分钟一次太长了。有时实时监控是必不可少的。您不必使用 FSW 来实现将在 C# 中侦听文件系统消息的东西(对于这些东西来说不是最方便的语言)。【参考方案14】:

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

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

【讨论】:

FileSystemWatcher 不仅在文件写入完成时发出通知。它通常会多次通知您“单个”逻辑写入,如果您在收到第一个通知后尝试打开文件,您将收到异常。【参考方案15】:

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

【讨论】:

【参考方案16】:

这个选项怎么样:

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

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

【讨论】:

如果写入文件的进程暂停超过一秒,或者在内存中缓冲超过一秒,那么您将得到另一个误报。我认为这在任何情况下都不是一个好的解决方案。

以上是关于等到文件在 .NET 中解锁的主要内容,如果未能解决你的问题,请参考以下文章

ORACLE用户的加锁解锁

在互斥锁解锁时取消阻止定时接收

Redis分布式锁解锁案例讲解

mysql innodb 行锁解锁后出现死锁

如何使用智能锁解锁/登录我的应用程序

在 oracle 19c 中解锁 SCOTT 用户