Почему File.Move позволяет двум потокам перемещать один и тот же файл одновременно?

В настоящее время у нас есть одно приложение, которое отслеживает папку для новых файлов. Чтобы сделать его отказоустойчивым и иметь возможность обрабатывать больше файлов одновременно, мы хотим иметь возможность запускать несколько экземпляров этого приложения на разных компьютерах. Мы используем File.Move для "блокировки" файла и убедитесь, что только один поток может обрабатывать файл за раз.

Чтобы проверить, что только одно приложение и/или поток могут выполнить File.Move в файле, я создал простое приложение (на основе исходного кода приложения), которое создало 10 потоков на приложение и контролировало папку, когда каждый thread обнаруживает новый файл, он выполняет File.Move на нем и изменяет расширение файла, чтобы попытаться остановить другой поток от того же самого.

Я видел проблему при запуске нескольких копий этого приложения (и он запускался сам по себе), в результате чего два потока (либо в одном приложении, либо разные) успешно выполняли File.Move без исключения, но поток, который выполнял его последний (я изменяю расширение файла, чтобы включить DateTime.Now.ToFileTime()), успешно переименовал файл. Я посмотрел, что делает File.Move, и он проверяет, существует ли файл до его выполнения, затем он вызывает Win32Native.MoveFile для выполнения перемещения.

Все остальные потоки/приложения вызывают исключение, как и я.

Причины, по которым это проблема:

  • Я думал, что только один поток может выполнять File.Move по файлу за раз.
  • Мне нужно надежно иметь только одно приложение/поток для обработки файла за раз.

Вот код, который выполняет File.Move:

public bool TryLock(string originalFile, out string changedFileName)
{
    FileInfo fileInfo = new FileInfo(originalFile);
    changedFileName = Path.ChangeExtension(originalFile, ".original." + DateTime.Now.ToFileTime());
    try
    {
        File.Move(originalFile, changedFileName);
    }
    catch (IOException ex)
    {
        Console.WriteLine("{3} - Thread {1}-{2} File {0} is already in use", fileInfo.Name, Thread.CurrentThread.ManagedThreadId, id, DateTime.Now.ToLongTimeString());
        return false;
    }
    catch (Exception ex)
    {
        Console.WriteLine("{3} - Thread {1}-{2} File {0} error {4}", fileInfo.Name, Thread.CurrentThread.ManagedThreadId, id, DateTime.Now.ToLongTimeString(), ex);
        return false;
    }
    return true;
}

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

Я запускаю Windows 7 Enterprise SP1 на SSD с NTFS.

Ответ 1

Основываясь на ответах @comments и @YuvalItzchakov на @marceln, я попробовал следующее, что, кажется, дает более надежные результаты:

using (var readFileStream = File.Open(originalFile, FileMode.Open, FileAccess.Read, FileShare.Delete))
{
    readFileStream.Lock(0, readFileStream.Length - 1);
    File.Move(originalFile, changedFileName);
    readFileStream.Unlock(0, readFileStream.Length - 1);
}

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

Ответ 2

Из описания MSDN Я предполагаю, что File.Move не открывает файл в эксклюзивном режиме.

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

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

File.Open(pathToYourFile, FileMode.Open, FileAccess.Read, FileShare.None);

Другие потоки не смогут открыть его, если операция перемещения уже выполняется. У вас могут быть проблемы с условиями гонки между моментом завершения копирования (таким образом, вам нужно избавиться от дескриптора файла) и удалить его.

Ответ 3

Использование File.Move в качестве блокировки не будет работать. Как указано в ответе @marceln, он не удалит исходный файл, который уже используется в другом месте и не имеет "блокирующего" поведения, вы не можете его ретранслировать.

Я бы предложил использовать BlockingCollection<T> для управления обработкой ваших файлов:

// Assuming this BlockingCollection is already filled with all string file paths
private BlockingCollection<string> _blockingFileCollection = new BlockingCollection<string>();

public bool TryProcessFile(string originalFile, out string changedFileName)
{
    FileInfo fileInfo = new FileInfo(originalFile);
    changedFileName = Path.ChangeExtension(originalFile, ".original." + DateTime.Now.ToFileTime());

    string itemToProcess;
    if (_blockingFileCollection.TryTake(out itemToProcess))
    {
        return false;
    }

    // The file should exclusively be moved by one thread,
    // all other should return false.

    File.Move(originalFile, changedFileName);
    return true;
}

Ответ 4

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

.

@usr В процессе создания, когда поток "заблокировал" файл, мы будем перемещать его по сетевым ресурсам.

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

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

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

Здесь альтернатива:

  • Скопируйте файл в целевую папку с другим расширением, которое игнорируется читателями.
  • Атомно переименуйте файл, чтобы удалить расширение

Переименование на том же томе всегда атомарно. Читатели могут получить ошибку нарушения совместного использования в течение очень короткого периода времени. Опять же, вам нужен цикл повторения или терпеть очень небольшое окно недоступности.