Chờ cho đến khi tệp được mở khóa trong .NET


103

Cách đơn giản nhất để chặn một chuỗi cho đến khi một tệp đã được mở khóa và có thể truy cập để đọc và đổi tên? Ví dụ, có một WaitOnFile () ở đâu đó trong .NET Framework không?

Tôi có một dịch vụ sử dụng FileSystemWatcher để tìm kiếm các tệp sẽ được truyền đến một trang FTP, nhưng sự kiện đã tạo tệp sẽ kích hoạt trước khi quá trình khác ghi xong tệp.

Giải pháp lý tưởng sẽ có một khoảng thời gian chờ để chuỗi không bị treo mãi mãi trước khi từ bỏ.

Chỉnh sửa: Sau khi thử một số giải pháp bên dưới, tôi đã thay đổi hệ thống để tất cả các tệp được ghi vào Path.GetTempFileName(), sau đó thực hiện File.Move()chuyển đến vị trí cuối cùng. Ngay sau khi FileSystemWatchersự kiện được kích hoạt, tệp đã hoàn tất.


4
Kể từ khi phát hành .NET 4.0, có cách nào tốt hơn để giải quyết vấn đề này không?
jason

Câu trả lời:


40

Đây là câu trả lời tôi đã đưa ra cho một câu hỏi liên quan :

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

8
Tôi thấy xấu xí này, nhưng giải pháp duy nhất có thể
knoopx

6
Điều này có thực sự hiệu quả trong trường hợp chung không? nếu bạn mở tệp trong mệnh đề using (), tệp sẽ được đóng và mở khóa khi phạm vi sử dụng kết thúc. Nếu có quy trình thứ hai sử dụng chiến lược tương tự như thế này (thử lại nhiều lần), thì sau khi thoát khỏi WaitForFile (), sẽ có một điều kiện chạy đua liên quan đến việc tệp có thể mở được hay không. Không?
Cheeso

75
Ý kiến ​​tồi! Mặc dù khái niệm này là đúng, nhưng giải pháp tốt hơn sẽ là trả về FileStream thay vì bool. Nếu tệp bị khóa lại trước khi người dùng có cơ hội lấy được khóa của mình trên tệp - anh ta sẽ nhận được ngoại lệ ngay cả khi hàm trả về "false"
Nissim 23/02/10

2
phương pháp của Fero ở đâu?
Vbp

1
Nhận xét của Nissim chính xác là những gì tôi cũng đang nghĩ nhưng nếu bạn định sử dụng tìm kiếm đó thì đừng quên đặt lại nó về 0 sau khi đọc byte. fs.Seek (0, SeekOrigin.Begin);
WHol

73

Bắt đầu từ câu trả lời của Eric, tôi đã bao gồm một số cải tiến để làm cho mã nhỏ gọn hơn và có thể tái sử dụng. Hy vọng nó hữu ích.

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
Tôi đến từ tương lai để nói rằng mã này vẫn hoạt động như một sự quyến rũ. Cảm ơn.
OnoSendai

6
@PabloCosta Chính xác! Nó không thể đóng nó, bởi vì nếu nó làm vậy, một luồng khác có thể chạy vào và mở nó, đánh bại mục đích. Việc triển khai này là chính xác vì nó luôn mở! Hãy để người gọi lo lắng về điều đó, sẽ an toàn khi usingở trạng thái null, chỉ cần kiểm tra null bên trong usingkhối.
doug65536

2
"FileStream fs = null;" nên được khai báo bên ngoài try nhưng bên trong for. Sau đó chỉ định và sử dụng fs bên trong thử. Khối bắt phải thực hiện "if (fs! = Null) fs.Dispose ();" (hoặc chỉ fs? .Dispose () trong C # 6) để đảm bảo FileStream không được trả về được dọn dẹp đúng cách.
Bill Menees

1
Có thực sự cần thiết để đọc một byte không? Theo kinh nghiệm của tôi nếu bạn đã mở tệp để truy cập đọc, bạn có nó, không cần phải kiểm tra nó. Mặc dù với thiết kế ở đây, bạn không buộc phải truy cập độc quyền, vì vậy thậm chí có thể bạn có thể đọc byte đầu tiên, nhưng không có người khác (khóa mức byte). Từ câu hỏi ban đầu, bạn có thể mở với mức chia sẻ chỉ đọc, vì vậy không quá trình nào khác có thể khóa hoặc sửa đổi tệp. Ở mức nào đó, tôi cảm thấy fs.ReadByte () là một sự lãng phí hoàn toàn, hoặc không đủ, tùy thuộc vào cách sử dụng.
eselk

8
Người dùng trường hợp nào có fsthể không bị rỗng trong catchkhối? Nếu hàm FileStreamtạo ném, biến sẽ không được gán giá trị và không có gì khác bên trong biến trycó thể ném giá trị IOException. Đối với tôi, có vẻ như nó sẽ ổn nếu chỉ cần làm return new FileStream(...).
Matti Virkkunen

18

Đây là một mã chung để làm điều này, độc lập với chính hoạt động của tệp. Đây là một ví dụ về cách sử dụng nó:

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

hoặc là

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

Bạn cũng có thể xác định số lần thử lại và thời gian chờ giữa các lần thử lại.

LƯU Ý: Thật không may, lỗi cơ bản của Win32 (ERROR_SHARING_VIOLATION) không hiển thị với .NET, vì vậy tôi đã thêm một hàm hack nhỏ ( IsSharingViolation) dựa trên cơ chế phản chiếu để kiểm tra điều này.

    /// <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
Họ thực sự có thể đã cung cấp một SharingViolationException. Trên thực tế, chúng vẫn có thể tương thích ngược, miễn là nó giảm dần IOException. Và họ thực sự, thực sự nên làm.
Roman Starkov


9
Trong .NET Framework 4.5, .NET Standard và .NET Core, HResult là thuộc tính công cộng trên lớp Exception. Sự suy ngẫm không còn cần thiết cho việc này. Từ 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

Tôi đã tập hợp một lớp người trợ giúp cho những thứ này. Nó sẽ hoạt động nếu bạn có quyền kiểm soát mọi thứ có thể truy cập tệp. Nếu bạn đang mong đợi sự tranh cãi từ nhiều thứ khác, thì điều này khá vô giá trị.

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
}

Nó hoạt động bằng cách sử dụng mutex được đặt tên. Những người muốn truy cập tệp sẽ cố gắng giành quyền kiểm soát mutex có tên, dùng chung tên tệp (với '\' s được chuyển thành '/' s). Bạn có thể sử dụng Open (), sẽ ngừng hoạt động cho đến khi có thể truy cập được mutex hoặc bạn có thể sử dụng TryOpen (TimeSpan), tính năng này sẽ cố lấy mutex trong khoảng thời gian nhất định và trả về false nếu không thể truy cập trong khoảng thời gian. Điều này rất có thể sẽ được sử dụng bên trong một khối đang sử dụng, để đảm bảo rằng các khóa được nhả đúng cách và luồng (nếu mở) sẽ được xử lý đúng cách khi đối tượng này được xử lý.

Tôi đã thực hiện một bài kiểm tra nhanh với ~ 20 thứ để thực hiện các lần đọc / ghi tệp khác nhau và không thấy lỗi nào. Rõ ràng là nó không cao cấp lắm, nhưng nó sẽ hoạt động cho phần lớn các trường hợp đơn giản.


5

Đối với ứng dụng cụ thể này, việc quan sát trực tiếp tệp chắc chắn sẽ dẫn đến một lỗi khó theo dõi, đặc biệt khi kích thước tệp tăng lên. Đây là hai chiến lược khác nhau sẽ hoạt động.

  • Ftp hai tệp nhưng chỉ xem một. Ví dụ: gửi các tệp important.txt và important.finish. Chỉ xem tệp hoàn thành nhưng xử lý txt.
  • FTP một tệp nhưng đổi tên nó khi hoàn tất. Ví dụ: send important.wait và yêu cầu người gửi đổi tên nó thành important.txt khi hoàn tất.

Chúc may mắn!


Điều đó ngược lại với tự động. Điều đó giống như lấy tệp theo cách thủ công, với nhiều bước hơn.
HackSlash

4

Một trong những kỹ thuật mà tôi đã sử dụng một thời gian trước là viết hàm của riêng tôi. Về cơ bản, hãy bắt ngoại lệ và thử lại bằng cách sử dụng bộ hẹn giờ mà bạn có thể kích hoạt trong một khoảng thời gian nhất định. Nếu có cách nào hay hơn, xin hãy chia sẻ.


3

Từ MSDN :

Sự kiện OnCreate được đưa ra ngay sau khi một tệp được tạo. Nếu một tệp đang được sao chép hoặc chuyển vào một thư mục đã xem, thì sự kiện OnChanged sẽ xuất hiện ngay lập tức, theo sau là một hoặc nhiều sự kiện OnChanged.

FileSystemWatcher của bạn có thể được sửa đổi để nó không thực hiện việc đọc / đổi tên trong sự kiện "OnCreate" mà thay vào đó:

  1. Kéo dài một chuỗi thăm dò trạng thái tệp cho đến khi nó không bị khóa (sử dụng đối tượng FileInfo)
  2. Gọi lại dịch vụ để xử lý tệp ngay khi xác định tệp không còn bị khóa và sẵn sàng hoạt động

1
Tạo chuỗi của trình theo dõi hệ thống tệp có thể dẫn đến bộ đệm bên dưới bị tràn, do đó thiếu nhiều tệp đã thay đổi. Cách tiếp cận tốt hơn sẽ là tạo hàng đợi người tiêu dùng / nhà sản xuất.
Nissim

2

Trong hầu hết các trường hợp, cách tiếp cận đơn giản như @harpo được đề xuất sẽ hoạt động. Bạn có thể phát triển mã phức tạp hơn bằng cách sử dụng phương pháp này:

  • Tìm tất cả các chốt đã mở cho tệp đã chọn bằng SystemHandleInformation \ SystemProcessInformation
  • Lớp con WaitHandle để có quyền truy cập vào tay cầm bên trong của nó
  • Chuyển các chốt được tìm thấy được bao bọc trong phương thức WaitHandle được phân lớp con đến phương thức WaitHandle.WaitAny

2

Quảng cáo để chuyển tệp kích hoạt quá trình SameNameASTrasferedFile.trg được tạo sau khi quá trình truyền tệp hoàn tất.

Sau đó, thiết lập FileSystemWatcher sẽ chỉ kích hoạt sự kiện trên tệp * .trg.


1

Tôi không biết bạn đang sử dụng cái gì để xác định trạng thái khóa của tệp, nhưng một cái gì đó như thế này sẽ làm được.

trong khi (đúng)
{
    thử {
        stream = File.Open (fileName, fileMode);
        phá vỡ;
    }
    bắt (FileIOException) {

        // kiểm tra xem đó có phải là vấn đề về khóa không

        Thread.Sleep (100);
    }
}

1
Hơi muộn, nhưng khi tệp bị khóa bằng cách nào đó, bạn sẽ không bao giờ thoát khỏi vòng lặp của mình. Bạn nên thêm một bộ đếm (xem câu trả lời đầu tiên).
Peter

0

Một giải pháp khả thi sẽ là kết hợp trình theo dõi hệ thống tệp với một số cuộc thăm dò ý kiến,

được Thông báo cho mọi Thay đổi trên Tệp và khi nhận được thông báo, hãy kiểm tra xem tệp đó có bị khóa như đã nêu trong câu trả lời hiện được chấp nhận hay không: https://stackoverflow.com/a/50800/6754146 Mã để mở luồng lọc được sao chép từ câu trả lời và sửa đổi một chút:

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

Với cách này, bạn có thể Kiểm tra một tệp nếu tệp đó bị khóa và nhận được thông báo khi tệp bị đóng trong lần gọi lại được chỉ định, bằng cách này bạn tránh được việc thăm dò quá tích cực và chỉ thực hiện công việc khi tệp có thể thực sự bị đóng.


-1

Tôi làm theo cách giống như Gulzar, chỉ cần tiếp tục thử với một vòng lặp.

Trên thực tế, tôi thậm chí không bận tâm đến trình theo dõi hệ thống tệp. Thăm dò ổ đĩa mạng cho các tệp mới mỗi phút một lần rất rẻ.


2
Nó có thể rẻ nhưng một phút một lần là quá lâu cho rất nhiều ứng dụng. Theo dõi thời gian thực đôi khi là cần thiết. Thay vì bạn phải triển khai một cái gì đó sẽ lắng nghe các thông báo Hệ thống tệp bằng C # (không phải là ngôn ngữ thuận tiện nhất cho những thứ này), bạn sử dụng FSW.
ThunderGr

-1

Chỉ cần sử dụng sự kiện Đã thay đổi với NotifyFilter NotifyFilters .

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

1
FileSystemWatcher không chỉ thông báo khi một tệp được ghi xong. Nó thường sẽ thông báo cho bạn nhiều lần đối với một lần ghi lôgic "duy nhất" và nếu bạn cố gắng mở tệp sau khi nhận được thông báo đầu tiên, bạn sẽ nhận được một ngoại lệ.
Ross

-1

Tôi đã gặp sự cố tương tự khi thêm tệp đính kèm triển vọng. "Sử dụng" đã lưu trong ngày.

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

Làm thế nào về điều này như một tùy chọn:

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

Tất nhiên nếu kích thước tệp được phân bổ trước khi tạo, bạn sẽ nhận được một kết quả dương tính giả.


1
Nếu quá trình ghi vào tệp tạm dừng hơn một giây hoặc bộ nhớ đệm trong hơn một giây, thì bạn sẽ nhận được một kết quả dương tính giả khác. Tôi không nghĩ rằng đây là một giải pháp tốt trong bất kỳ hoàn cảnh nào.
Chris Wenham
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.