Подождите, пока файл не будет разблокирован в .NET

Какой самый простой способ заблокировать поток до тех пор, пока файл не будет разблокирован и доступен для чтения и переименования? Например, есть ли где-нибудь в .NET Framework WaitOnFile ()?

У меня есть служба, которая использует FileSystemWatcher для поиска файлов, которые должны быть переданы на FTP-сайт, но событие file created срабатывает до того, как другой процесс закончит запись файла.

Идеальное решение - это тайм-аут, чтобы нить не зависала вечно, прежде чем сдаться.

Изменить: опробовав некоторые из приведенных ниже решений, я закончил тем, что изменил систему, чтобы все файлы записывались в Path.GetTempFileName(), а затем выполняли File.Move() в окончательное местоположение. Как только произошло событие FileSystemWatcher, файл был уже готов.


person Chris Wenham    schedule 08.09.2008    source источник
comment
Есть ли лучший способ решить эту проблему с момента выпуска .NET 4.0?   -  person jason    schedule 22.11.2010


Ответы (16)


Это был ответ, который я дал на связанный с этим вопрос:

    /// <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;
    }
person Eric Z Beard    schedule 08.09.2008
comment
Я считаю это уродливым, но единственно возможным решением - person knoopx; 29.04.2009
comment
Это действительно сработает в общем случае? если вы открываете файл в предложении using (), файл закрывается и разблокируется при завершении области действия using. Если есть второй процесс, использующий ту же стратегию, что и этот (повторять несколько раз), то после выхода из WaitForFile () возникает состояние гонки относительно того, будет ли файл открываться или нет. Нет? - person Cheeso; 14.06.2009
comment
Вы говорите о двух потоках в одном приложении, вызывающих WaitForFile в одном файле? Хм, не уверен, так как я в основном использую это, чтобы дождаться, пока другие процессы отпустят файл. Этот код у меня уже давно работает, и он мне хорошо подходит. Должно быть довольно просто написать приложение для проверки вашей теории. - person Eric Z Beard; 15.06.2009
comment
Плохая идея! Хотя концепция верна, лучшим решением будет возврат FileStream вместо bool. Если файл снова заблокирован до того, как пользователь получил возможность заблокировать файл, он получит исключение, даже если функция вернула false. - person Nissim; 23.02.2010
comment
Я также считаю, что это некрасиво, и нахожу ниже ответ Феро, который делает все, что делает этот, элегантно и за небольшую часть кода. Кто-нибудь хочет прокомментировать любые подводные камни в методе Феро? Я сам собираюсь реализовать нечто подобное. - person Mike Chamberlain; 13.10.2010
comment
где метод Феро? - person Vbp; 15.07.2014
comment
Комментарий Ниссима - это именно то, о чем я тоже думал, но если вы собираетесь использовать этот поиск, не забудьте сбросить его на 0 после чтения байта. fs.Seek (0, SeekOrigin.Begin); - person WHol; 27.08.2015
comment
попробуйте поймать, это ресурсоемкий процесс, если мы каким-то образом сможем его избежать, лучше всего, поскольку мы пишем такой код, это означает, что мы ожидаем, что это будет происходить часто, и это означает множество исключений, и это приведет к сильному перегреву процессора . - person deadManN; 13.09.2017
comment
есть ли способ преобразовать в использование обработчика ожидания вместо thread.sleep? - person Robert Koernke; 20.01.2021
comment
Разве размер буфера 1 не имеет больше смысла? - person Simon; 28.02.2021

Начиная с ответа Эрика, я включил некоторые улучшения, чтобы сделать код более компактным и многоразовым. Надеюсь, это будет полезно.

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;
}
person mafu    schedule 09.09.2010
comment
Я пришел из будущего, чтобы сказать, что этот код по-прежнему работает как шарм. Спасибо. - person OnoSendai; 02.12.2013
comment
Работает очень хорошо. Но вы не закрываете поток, поэтому у вас могут возникнуть проблемы с доступом к этим файлам позже (например, если вы планируете стереть их после чтения). - person Pablo Costa; 25.03.2016
comment
@PabloCosta Вызывающий, вероятно, должен просто заключить его в блок using. Конечно, нельзя просто забыть о стриме. - person mafu; 26.03.2016
comment
@PabloCosta Совершенно верно! Он не может закрыть его, потому что, если бы это произошло, другой поток мог бы ворваться и открыть его, нарушив цель. Эта реализация верна, потому что она остается открытой! Позвольте вызывающему абоненту беспокоиться об этом, using на нуле безопасно, просто проверьте на ноль внутри блока using. - person doug65536; 03.10.2016
comment
FileStream fs = null; следует объявить вне try, но внутри for. Затем назначьте и используйте fs внутри попытки. Блок catch должен работать if (fs! = Null) fs.Dispose (); (или просто fs? .Dispose () в C # 6), чтобы гарантировать, что FileStream, который не возвращается, очищен должным образом. - person Bill Menees; 10.10.2016
comment
@BillMenees Спасибо, хорошо заметили. Фиксированный! - person mafu; 10.10.2016
comment
Неужели нужно читать байт? По моему опыту, если вы открыли файл для чтения, он у вас есть, не нужно его проверять. Несмотря на то, что здесь вы не форсируете монопольный доступ, вы даже можете прочитать первый байт, но не другие (блокировка байтового уровня). Исходя из исходного вопроса, вы, вероятно, откроете с уровнем общего доступа только для чтения, поэтому никакой другой процесс не может заблокировать или изменить файл. В любом случае, я считаю, что fs.ReadByte () либо бесполезная трата, либо недостаточная, в зависимости от использования. - person eselk; 17.02.2017
comment
@eselk Хороший вопрос. Я забыл, как тогда это проверял. Вдобавок было бы также странно читать байт при открытии в режиме только для записи. - person mafu; 06.03.2017
comment
Прямо сейчас я не могу придумать сценарий, в котором чтение байтов действительно полезно, поэтому я удалил его. Если другие обнаружат проблемы с этим, дайте мне знать. - person mafu; 06.03.2017
comment
Пользователь, какое обстоятельство может fs быть ненулевым в блоке catch? Если конструктор FileStream выдает ошибку, переменной не будет присвоено значение, и внутри try нет ничего, что могло бы вызвать IOException. Мне кажется, что можно просто сделать return new FileStream(...). - person Matti Virkkunen; 22.03.2017
comment
Спустя 8 с половиной лет этот код все еще работает! Спасибо @mafu :) - person Scotty; 14.02.2019
comment
Надежный код, по-прежнему работает как шарм - person Ahmed Osama; 27.04.2021

Вот общий код для этого, не зависящий от самой операции с файлом. Это пример того, как его использовать:

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

or

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

Вы также можете определить количество повторных попыток и время ожидания между повторными попытками.

ПРИМЕЧАНИЕ. К сожалению, основная ошибка Win32 (ERROR_SHARING_VIOLATION) не отображается в .NET, поэтому я добавил небольшую функцию взлома (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;
    }
person Simon Mourier    schedule 26.03.2011
comment
Они действительно могли предоставить SharingViolationException. Фактически, они все еще могут быть обратно совместимыми, если они происходят от IOException. И им действительно, действительно следует. - person Roman Starkov; 23.05.2011
comment
Marshal.GetHRForException msdn.microsoft.com/en -us / library / - person Steven T. Cramer; 03.07.2013
comment
В .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. - person NightOwl888; 20.09.2017

Я собрал вспомогательный класс для таких вещей. Он будет работать, если у вас есть контроль над всем, что имеет доступ к файлу. Если вы ожидаете разногласий от множества других вещей, то это бесполезно.

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 объектами для различных операций чтения / записи файла и не обнаружил повреждений. Очевидно, что он не очень продвинутый, но он должен работать в большинстве простых случаев.

person user152791    schedule 07.08.2009

Для этого конкретного приложения непосредственное наблюдение за файлом неизбежно приведет к трудно отслеживаемой ошибке, особенно при увеличении размера файла. Вот две разные стратегии, которые будут работать.

  • Ftp два файла а смотреть только один. Например, отправьте файлы important.txt и important.finish. Следите только за файлом финиша, но обрабатывайте txt.
  • FTP один файл, но переименуйте его, когда закончите. Например, отправьте important.wait и попросите отправителя переименовать его в important.txt, когда закончите.

Удачи!

person jason saldo    schedule 09.09.2008
comment
Это противоположно автоматическому. Это похоже на получение файла вручную с дополнительными шагами. - person HackSlash; 18.04.2019

Некоторое время назад я использовал один из приемов - написание собственной функции. В основном перехватите исключение и повторите попытку, используя таймер, который вы можете запустить в течение определенного времени. Если есть способ получше, поделитесь, пожалуйста.

person Gulzar Nazim    schedule 08.09.2008

Из MSDN:

Событие OnCreated возникает при создании файла. Если файл копируется или переносится в отслеживаемый каталог, немедленно возникает событие OnCreated, за которым следует одно или несколько событий OnChanged.

Ваш FileSystemWatcher можно изменить так, чтобы он не выполнял чтение / переименование во время события «OnCreated», а:

  1. Охватывает поток, который опрашивает состояние файла до тех пор, пока он не будет заблокирован (с использованием объекта FileInfo)
  2. Обратный вызов службы для обработки файла, как только он определяет, что файл больше не заблокирован и готов к работе.
person Guy Starbuck    schedule 08.09.2008
comment
Создание потока наблюдателя файловой системы может привести к переполнению нижележащего буфера, что приведет к потере большого количества измененных файлов. Лучшим подходом будет создание очереди потребителя / производителя. - person Nissim; 23.02.2010

В большинстве случаев будет работать простой подход, например, предложенный @harpo. Используя этот подход, вы можете разработать более сложный код:

  • Найдите все открытые дескрипторы для выбранного файла с помощью SystemHandleInformation \ SystemProcessInformation
  • Подкласс WaitHandle, чтобы получить доступ к его внутреннему дескриптору
  • Передача найденных дескрипторов, заключенных в подкласс WaitHandle, в метод WaitHandle.WaitAny
person aku    schedule 09.09.2008

Объявление для передачи файла триггера процесса SameNameASTrasferFile.trg, который создается после завершения передачи файла.

Затем настройте FileSystemWatcher, который будет запускать событие только для файла * .trg.

person Rudi    schedule 08.07.2011

Я не знаю, что вы используете для определения статуса блокировки файла, но что-то вроде этого должно это сделать.

while (true)
{
    try {
        stream = File.Open( fileName, fileMode );
        break;
    }
    catch( FileIOException ) {

        // check whether it's a lock problem

        Thread.Sleep( 100 );
    }
}
person harpo    schedule 08.09.2008
comment
Немного поздно, но когда файл каким-то образом заблокирован, вы никогда не выйдете из цикла. Вам следует добавить счетчик (см. 1-й ответ). - person Peter; 14.12.2018

Возможным решением было бы объединить наблюдатель файловой системы с некоторым опросом,

получать уведомление о каждом изменении в файле, и при получении уведомления проверьте, заблокирован ли он, как указано в принятом на данный момент ответе: 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);
}

Таким образом вы можете проверить файл, если он заблокирован, и получить уведомление, когда он закрыт по указанному обратному вызову, таким образом вы избегаете чрезмерно агрессивного опроса и выполняете работу только тогда, когда он может быть фактически закрыт

person Florian K    schedule 14.03.2017

Вот ответ, аналогичный приведенному выше, за исключением того, что я добавил проверку, существует ли файл.

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;
        }
person Tyrone Moodley    schedule 06.05.2021

Я делаю это так же, как Гульзар, просто продолжаю пробовать петлей.

На самом деле я даже не беспокоюсь о наблюдателе файловой системы. Опрашивать сетевой диск о новых файлах раз в минуту - это дешево.

person Jonathan Allen    schedule 08.09.2008
comment
Это может быть дешево, но раз в минуту - слишком долго для многих приложений. Иногда необходим мониторинг в реальном времени. Вместо того, чтобы реализовать что-то, что будет прослушивать сообщения файловой системы на C # (не самый удобный язык для этих вещей), вы используете FSW. - person ThunderGr; 07.11.2013

Просто используйте событие Changed с NotifyFilter NotifyFilters.LastWrite:

var watcher = new FileSystemWatcher {
      Path = @"c:\temp\test",
      Filter = "*.xml",
      NotifyFilter = NotifyFilters.LastWrite
};
watcher.Changed += watcher_Changed; 
watcher.EnableRaisingEvents = true;
person Bernhard Hochgatterer    schedule 22.01.2013
comment
FileSystemWatcher не только уведомляет о завершении записи в файл. Он часто уведомляет вас несколько раз об одной логической записи, и если вы попытаетесь открыть файл после получения первого уведомления, вы получите исключение. - person Ross; 01.05.2013

Я столкнулся с аналогичной проблемой при добавлении вложения 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);
person Jahmal23    schedule 04.03.2014

Как насчет этого как варианта:

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

Конечно, если размер файла предварительно выделен при создании, вы получите ложное срабатывание.

person Ralph Shillington    schedule 23.07.2009
comment
Если процесс записи в файл приостанавливается более чем на секунду или буферизуется в памяти более чем на секунду, вы получите еще одно ложное срабатывание. Я не думаю, что это хорошее решение ни при каких обстоятельствах. - person Chris Wenham; 23.07.2009