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

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

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

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

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

Есть ли лучший способ решить эту проблему с момента выпуска .NET 4.0?

jason 22.11.2010 23:48
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
107
1
82 784
15
Перейти к ответу Данный вопрос помечен как решенный

Ответы 15

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

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

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

        // check whether it's a lock problem

        Thread.Sleep( 100 );
    }
}

Немного поздно, но когда файл каким-то образом заблокирован, вы никогда не выйдете из цикла. Вам следует добавить счетчик (см. 1-й ответ).

Peter 14.12.2018 12:56

От 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», а:

  1. Охватывает поток, который опрашивает состояние файла до тех пор, пока он не будет заблокирован (с использованием объекта FileInfo)
  2. Обратный вызов службы для обработки файла, как только он определяет, что файл больше не заблокирован и готов к работе.

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

Nissim 23.02.2010 12:29
Ответ принят как подходящий

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

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

Я считаю это уродливым, но единственно возможным решением

knoopx 29.04.2009 13:31

Это действительно сработает в общем случае? если вы открываете файл в предложении using (), файл закрывается и разблокируется при завершении области действия using. Если есть второй процесс, использующий ту же стратегию, что и этот (повторять несколько раз), то после выхода из WaitForFile () возникает состояние гонки относительно того, будет ли файл открываться или нет. Нет?

Cheeso 14.06.2009 01:11

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

Eric Z Beard 15.06.2009 23:40

Плохая идея! Хотя концепция верна, лучшим решением будет возврат FileStream вместо bool. Если файл снова заблокирован до того, как пользователь получил возможность заблокировать файл, он получит исключение, даже если функция вернула «false».

Nissim 23.02.2010 12:26

Я также считаю, что это некрасиво, и нахожу ниже ответ Феро, который делает все, что делает этот, элегантно и за небольшую часть кода. Кто-нибудь хочет прокомментировать любые подводные камни в методе Феро? Я сам собираюсь реализовать нечто подобное.

Mike Chamberlain 13.10.2010 10:56

где метод Феро?

Vbp 15.07.2014 00:10

Комментарий Ниссима - это именно то, о чем я тоже думал, но если вы собираетесь использовать этот поиск, не забудьте сбросить его на 0 после чтения байта. fs.Seek (0, SeekOrigin.Begin);

WHol 27.08.2015 16:42

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

deadManN 13.09.2017 10:12

есть ли способ преобразовать в использование обработчика ожидания вместо thread.sleep?

Robert Koernke 20.01.2021 00:44

Разве размер буфера 1 не имеет больше смысла?

Simon 28.02.2021 02:00

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

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

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

ThunderGr 07.11.2013 11:11

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

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

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

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

Удачи!

Это противоположно автоматическому. Это похоже на получение файла вручную с дополнительными шагами.

HackSlash 18.04.2019 20:45

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

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

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

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

Chris Wenham 23.07.2009 17:44

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

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

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

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

Я пришел из будущего, чтобы сказать, что этот код по-прежнему работает как шарм. Спасибо.

OnoSendai 02.12.2013 20:31

Работает очень хорошо. Но вы не закрываете поток, поэтому у вас могут возникнуть проблемы с доступом к этим файлам позже (например, если вы планируете стереть их после чтения).

Pablo Costa 25.03.2016 16:50

@PabloCosta Вызывающий, вероятно, должен просто заключить его в блок using. Конечно, нельзя просто забыть о стриме.

mafu 26.03.2016 20:48

@PabloCosta Совершенно верно! Он не могу закрывает его, потому что, если это так, другой поток может ворваться и открыть его, нарушив цель. Эта реализация верна, потому что она остается открытой! Позвольте вызывающему абоненту беспокоиться об этом, это безопасно для using с нулевым значением, просто проверьте наличие null внутри блока using.

doug65536 03.10.2016 03:41

"FileStream fs = null;" следует объявить вне try, но внутри for. Затем назначьте и используйте fs внутри попытки. Блок catch должен выполнять "if (fs! = Null) fs.Dispose ();" (или просто fs? .Dispose () в C# 6), чтобы гарантировать, что FileStream, который не возвращается, очищен должным образом.

Bill Menees 10.10.2016 14:19

@BillMenees Спасибо, хорошо заметили. Фиксированный!

mafu 10.10.2016 18:21

Неужели нужно читать байт? По моему опыту, если вы открыли файл для чтения, он у вас есть, не нужно его проверять. Несмотря на то, что здесь вы не используете монопольный доступ, вы даже можете прочитать первый байт, но не другие (блокировка байтового уровня). Исходя из исходного вопроса, вы, вероятно, откроете с уровнем общего доступа только для чтения, поэтому никакой другой процесс не может заблокировать или изменить файл. В любом случае, я считаю, что fs.ReadByte () либо бесполезная трата, либо недостаточная, в зависимости от использования.

eselk 17.02.2017 06:23

@eselk Хорошее замечание. Я забыл, как тогда это проверял. К тому же было бы странно читать байт при открытии в режиме только для записи.

mafu 06.03.2017 21:37

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

mafu 06.03.2017 21:41

Пользователь, при каких обстоятельствах fs может быть не нулевым в блоке catch? Если конструктор FileStream выдает ошибку, переменной не будет присвоено значение, и внутри try нет ничего, что могло бы вызвать IOException. Мне кажется, вполне нормально просто использовать return new FileStream(...).

Matti Virkkunen 22.03.2017 19:28

Спустя 8 с половиной лет этот код все еще работает! Спасибо @mafu :)

Scotty 14.02.2019 15:24

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

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

или же

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

Они действительно могли предоставить SharingViolationException. Фактически, они все еще могут быть обратно совместимыми, если они происходят от IOException. И им действительно, действительно следует.

Roman Starkov 23.05.2011 21:50

Marshal.GetHRForException msdn.microsoft.com/en-us/library/…

Steven T. Cramer 03.07.2013 19:34

В .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.

NightOwl888 20.09.2017 11:03

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

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

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

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

FileSystemWatcher не только уведомляет о завершении записи в файл. Он часто уведомляет вас несколько раз об «одиночной» логической записи, и если вы попытаетесь открыть файл после получения первого уведомления, вы получите исключение.

Ross 01.05.2013 11:18

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

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

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

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

Другие вопросы по теме