首页 / 知识

等待文件在.NET中解锁

2023-04-14 15:02:00

等待文件在.NET中解锁

Wait until file is unlocked in .NET

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

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

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

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


从Eric的答案开始,我进行了一些改进,以使代码更加紧凑和可重复使用。希望它有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
    /// <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;
    }


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

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

要么

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
    /// <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;
    }

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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件事进行了快速测试,以对文件进行各种读/写操作,未发现损坏。显然,它不是很高级,但是它应该适用于大多数简单情况。


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

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

祝好运!


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


从MSDN:

The OnCreated event is raised as soon
as a file is created. If a file is
being copied or transferred into a
watched directory, the OnCreated event
will be raised immediately, followed
by one or more OnChanged events.

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

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

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

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


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

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

    我不知道您要使用什么来确定文件的锁定状态,但是应该执行类似的操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    while (true)
    {
        try {
            stream = File.Open( fileName, fileMode );
            break;
        }
        catch( FileIOException ) {

            // check whether it's a lock problem

            Thread.Sleep( 100 );
        }
    }

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

    通知文件上的每个更改,并在收到通知时检查是否
    如当前接受的答案所述锁定:https://stackoverflow.com/a/50800/6754146
    打开文件流的代码是从答案中复制并稍作修改的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    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);
    }

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


    添加Outlook附件时,我遇到了类似的问题。"使用"节省了一天。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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);

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

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

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

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


    如何作为一个选择:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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);
        }
    }

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


    文件解锁等待读取

    最新内容

    相关内容

    猜你喜欢