Какой самый простой способ заблокировать поток до тех пор, пока файл не будет разблокирован и доступен для чтения и переименования? Например, есть ли где-нибудь в .NET Framework WaitOnFile ()?
У меня есть служба, которая использует FileSystemWatcher для поиска файлов, которые должны быть переданы на FTP-сайт, но событие файл создан срабатывает до того, как другой процесс закончит запись файла.
Идеальное решение было бы иметь период тайм-аута, чтобы поток не зависал навсегда, прежде чем сдаться.
Обновлено: опробовав некоторые из приведенных ниже решений, я изменил система, чтобы все файлы записывались в Path.GetTempFileName(), а затем выполняли File.Move() в окончательном месте. Как только произошло событие FileSystemWatcher, файл был уже готов.





Некоторое время назад я использовал один из приемов - написание собственной функции. В основном перехватите исключение и повторите попытку, используя таймер, который вы можете запустить в течение определенного времени. Если есть способ получше, поделитесь, пожалуйста.
Я не знаю, что вы используете для определения статуса блокировки файла, но что-то вроде этого должно это сделать.
while (true)
{
try {
stream = File.Open( fileName, fileMode );
break;
}
catch( FileIOException ) {
// check whether it's a lock problem
Thread.Sleep( 100 );
}
}
Немного поздно, но когда файл каким-то образом заблокирован, вы никогда не выйдете из цикла. Вам следует добавить счетчик (см. 1-й ответ).
От 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», а:
Создание потока наблюдателя файловой системы может привести к переполнению нижележащего буфера, что приведет к потере большого количества измененных файлов. Лучшим подходом будет создание очереди потребителя / производителя.
Это был ответ, который я дал на связанный вопрос:
/// <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 () возникает состояние гонки относительно того, будет ли файл открываться или нет. Нет?
Вы говорите о двух потоках в одном приложении, вызывающих WaitForFile в одном файле? Хм, не уверен, так как я в основном использую это, чтобы дождаться, пока другие процессы отпустят файл. Этот код у меня уже давно работает, и он мне хорошо подходит. Должно быть довольно просто написать приложение для проверки вашей теории.
Плохая идея! Хотя концепция верна, лучшим решением будет возврат FileStream вместо bool. Если файл снова заблокирован до того, как пользователь получил возможность заблокировать файл, он получит исключение, даже если функция вернула «false».
Я также считаю, что это некрасиво, и нахожу ниже ответ Феро, который делает все, что делает этот, элегантно и за небольшую часть кода. Кто-нибудь хочет прокомментировать любые подводные камни в методе Феро? Я сам собираюсь реализовать нечто подобное.
где метод Феро?
Комментарий Ниссима - это именно то, о чем я тоже думал, но если вы собираетесь использовать этот поиск, не забудьте сбросить его на 0 после чтения байта. fs.Seek (0, SeekOrigin.Begin);
попробуйте поймать, это ресурсоемкий процесс, если мы каким-то образом сможем его избежать, это лучше всего, поскольку мы пишем такой код, это означает, что мы ожидаем, что это будет происходить часто, и это означает множество исключений, и это приведет к сильному перегреву процессора .
есть ли способ преобразовать в использование обработчика ожидания вместо thread.sleep?
Разве размер буфера 1 не имеет больше смысла?
Я делаю это так же, как Гульзар, просто продолжаю пробовать петлей.
На самом деле я даже не беспокоюсь о наблюдателе файловой системы. Опрашивать сетевой диск о новых файлах раз в минуту - это дешево.
Это может быть дешево, но раз в минуту - слишком долго для многих приложений. Иногда необходим мониторинг в реальном времени. Вместо того, чтобы реализовать что-то, что будет прослушивать сообщения файловой системы на C# (не самый удобный язык для этих вещей), вы используете FSW.
В большинстве случаев будет работать простой подход, например, предложенный @harpo. Используя этот подход, вы можете разработать более сложный код:
Для этого конкретного приложения непосредственное наблюдение за файлом неизбежно приведет к трудно отслеживаемой ошибке, особенно при увеличении размера файла. Вот две разные стратегии, которые будут работать.
Удачи!
Это противоположно автоматическому. Это похоже на получение файла вручную с дополнительными шагами.
Как насчет этого как варианта:
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);
}
}
Конечно, если размер файла предварительно выделен при создании, вы получите ложное срабатывание.
Если процесс записи в файл приостанавливается более чем на секунду или буферизуется в памяти более секунды, вы получите еще одно ложное срабатывание. Я не думаю, что это хорошее решение ни при каких обстоятельствах.
Я собрал вспомогательный класс для таких вещей. Это сработает, если вы контролируете все, что имеет доступ к файлу. Если вы ожидаете разногласий от множества других вещей, то это бесполезно.
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;
}
Я пришел из будущего, чтобы сказать, что этот код по-прежнему работает как шарм. Спасибо.
Работает очень хорошо. Но вы не закрываете поток, поэтому у вас могут возникнуть проблемы с доступом к этим файлам позже (например, если вы планируете стереть их после чтения).
@PabloCosta Вызывающий, вероятно, должен просто заключить его в блок using. Конечно, нельзя просто забыть о стриме.
@PabloCosta Совершенно верно! Он не могу закрывает его, потому что, если это так, другой поток может ворваться и открыть его, нарушив цель. Эта реализация верна, потому что она остается открытой! Позвольте вызывающему абоненту беспокоиться об этом, это безопасно для using с нулевым значением, просто проверьте наличие null внутри блока using.
"FileStream fs = null;" следует объявить вне try, но внутри for. Затем назначьте и используйте fs внутри попытки. Блок catch должен выполнять "if (fs! = Null) fs.Dispose ();" (или просто fs? .Dispose () в C# 6), чтобы гарантировать, что FileStream, который не возвращается, очищен должным образом.
@BillMenees Спасибо, хорошо заметили. Фиксированный!
Неужели нужно читать байт? По моему опыту, если вы открыли файл для чтения, он у вас есть, не нужно его проверять. Несмотря на то, что здесь вы не используете монопольный доступ, вы даже можете прочитать первый байт, но не другие (блокировка байтового уровня). Исходя из исходного вопроса, вы, вероятно, откроете с уровнем общего доступа только для чтения, поэтому никакой другой процесс не может заблокировать или изменить файл. В любом случае, я считаю, что fs.ReadByte () либо бесполезная трата, либо недостаточная, в зависимости от использования.
@eselk Хорошее замечание. Я забыл, как тогда это проверял. К тому же было бы странно читать байт при открытии в режиме только для записи.
Прямо сейчас я не могу придумать сценарий, в котором чтение байтов действительно полезно, поэтому я удалил его. Если другие обнаружат проблемы с этим, дайте мне знать.
Пользователь, при каких обстоятельствах fs может быть не нулевым в блоке catch? Если конструктор FileStream выдает ошибку, переменной не будет присвоено значение, и внутри try нет ничего, что могло бы вызвать IOException. Мне кажется, вполне нормально просто использовать return new FileStream(...).
Спустя 8 с половиной лет этот код все еще работает! Спасибо @mafu :)
Вот общий код для этого, не зависящий от самой операции с файлом. Это пример того, как его использовать:
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. И им действительно, действительно следует.
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.
Объявление для переноса файла триггера процесса 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 не только уведомляет о завершении записи в файл. Он часто уведомляет вас несколько раз об «одиночной» логической записи, и если вы попытаетесь открыть файл после получения первого уведомления, вы получите исключение.
Я столкнулся с аналогичной проблемой при добавлении вложения 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);
}
Таким образом, вы можете проверить файл, если он заблокирован, и получить уведомление, когда он закрыт по указанному обратному вызову, таким образом вы избегаете чрезмерно агрессивного опроса и выполняете работу только тогда, когда он может быть фактически закрыт.
Есть ли лучший способ решить эту проблему с момента выпуска .NET 4.0?