Заблокировать файл, а затем удалить/переместить его

Я реализую класс на С#, который должен отслеживать каталог, обрабатывать файлы по мере их удаления, а затем удалять (или перемещать) обработанный файл, как только обработка завершена. Поскольку для этого кода может быть несколько потоков, первая из которых берет файл, блокирует его исключительно, поэтому никакие другие потоки не будут читать один и тот же файл , и никакой внешний процесс или пользователь не могут каким-либо образом получить доступ, Я хотел бы сохранить блокировку до тех пор, пока файл не будет удален/перемещен, поэтому нет риска для другого потока/процесса/пользователя, обращающегося к нему.

До сих пор я пробовал 2 варианта реализации, но ни один из них не работает так, как я хочу.

Вариант 1

FileStream fs = file.Open(FileMode.Open, FileAccess.Read, FileShare.Delete);
//Read and process
File.Delete(file.FullName); //Or File.Move, based on a flag
fs.Close();

Вариант 2

FileStream fs = file.Open(FileMode.Open, FileAccess.Read, FileShare.None);
//Read and process
fs.Close();
File.Delete(file.FullName); //Or File.Move, based on a flag

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

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

Я искал какой-то API, который может выполнить удаление с помощью дескриптора файла. У меня уже есть эксклюзивный доступ.

Изменить

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

Ответ 1

Приходят в голову два решения.

Первое и самое простое - чтобы поток переименовал файл в то, что другие потоки не будут касаться. Что-то вроде "filename.dat.<unique number>", где <unique number> - это то, что связано с потоком. Затем поток может вставить в файл все, что ему нужно.

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

Другой способ состоит в том, чтобы один поток контролировал каталог и помещал имена файлов в BlockingCollection. Рабочие потоки берут элементы из этой очереди и обрабатывают их. Поскольку только один поток может получить этот конкретный элемент из очереди, нет никаких утверждений.

Решение BlockingCollection немного (но немного) более сложно настроить, но должно работать лучше, чем решение, которое имеет несколько потоков, контролирующих один и тот же каталог.

Изменить

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

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

File.Move(sourceFilename, destFilename);
// the file is now in a presumably safe place.
// Assuming that all of your threads obey the rules,
// you have exclusive access by agreement.

Изменить # 2

Другая возможность - открыть файл исключительно и скопировать его с помощью собственного цикла копирования, оставив файл открытым, когда копия будет выполнена. Затем вы можете перемотать файл и выполнить обработку. Что-то вроде:

var srcFile = File.Open(/* be sure to specify exclusive access */);
var destFile = File.OpenWrite(/* destination path */);
// copy the file
var buffer = new byte[32768];
int bytesRead = 0;
while ((bytesRead = srcFile.Read(buffer, 0, buffer.Length)) != 0)
{
    destFile.Write(buffer, 0, bytesRead);
}
// close destination
destFile.Close();
// rewind source
srcFile.Seek(0, SeekOrigin.Start);
// now read from source to do your processing.
// for example, to get a StreamReader, just pass the srcFile stream to the constructor.

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

using (var strm = new StreamReader(srcStream, ...))
{
    // do stuff here
}

Это заканчивает закрытие потока и srcStream. Вы должны написать свой код следующим образом:

using (var srcStream = new FileStream( /* exclusive access */))
{
    var reader = new StreamReader(srcStream, ...);
    // process the stream, leaving the reader open
    // rewind srcStream
    // copy srcStream to destination
    // close reader
}

Достойный, но неуклюжий.

О, и если вы хотите устранить потенциал кого-либо, читающего файл, прежде чем вы сможете его удалить, просто обрезайте файл на 0, прежде чем вы его закроете. Как в:

srcStream.Seek(0, SeekOrigin.Begin);
srcStream.SetLength(0);

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

Ответ 2

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

Ответ 3

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

Ответ 4

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

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

Код Psuedo:

try
{
    // get an exclusive cross-server/process/thread lock by opening/creating a temp file with no sharing allowed
    var lockFilePath = $"{file}.lck";
    var lockFile = File.Open(lockFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);

    try
    {
        // open file itself with no sharing allowed, in case some process that does not use our locking schema is trying to use it
        var fileHandle = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.None);

        // TODO: add processing -- we have exclusive access to the file, and also the locking file

        fileHandle.Close();

        // at this point it is possible for some other process that does not use our locking schema to lock the file before we
        //  move it, causing us to process this file again -- we would always have to handle issues where we failed to move
        //  the file anyway (maybe we just lost power, or crashed?) so we had to design around this no matter what

        File.Move(file, archiveDestination);
    }
    finally
    {
        lockFile.Close();

        try
        {
            File.Delete(lockFilePath);
        }
        catch (Exception ex)
        {
            // another process opened locked file after we closed it, before it was deleted -- safely ignore, other process will delete lock file
        }
    }
}
catch (Exception ex)
{
    // another process already has exclusive access to the lock file, we don't need to do anything
    // or we failed while processing, in which case we did not move the file so it will be tried again by this process or another
}

Одна приятная вещь в этом шаблоне - это также можно использовать в тех случаях, когда блокировка поддерживается файловым хранилищем. Например, если вы пытались обрабатывать файлы на FTP/SFTP-сервере, вы могли бы сделать ваши временные файлы блокировки использующими обычный диск (или общий ресурс SMB), поскольку блокирующие файлы не должны находиться в том же месте, что и файлы сами.

Я не могу взять на себя ответственность за эту идею, она была дольше, чем ПК, и используется множеством приложений, таких как Microsoft Word, Excel, Access и большинство старых систем баз данных. Читайте: хорошо протестировано.

Ответ 5

Это решение, которое считается не на 100% водонепроницаемым, вполне может дать вам то, что вам нужно. (Это для нас.)

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

FileInfo file = ...

// Get read access to the file and only allow other processes write or delete access.
// Keeps others from locking the file for reading.
var readStream = file.Open(FileMode.Open, FileAccess.Read, FileShare.Write | FileShare.Delete);
FileStream preventWriteAndDelete;
try
{
    // Now try to get a lock on than only allows others to read the file.  We can acquire both
    // locks because they each allow the other.  Together, they give us exclusive access to the
    // file.
    preventWriteAndDelete = file.Open(FileMode.Open, FileAccess.Write, FileShare.Read);
}
catch
{
    // We couldn't get the second lock, so release the first.
    readStream.Dispose();
    throw;
}

Теперь вы можете прочитать файл (с помощью readStream). Если вам нужно написать в него, вам придется сделать это с другим потоком.

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

preventWriteAndDelete.Dispose(); // Release lock that prevents deletion.
file.Delete();
// This lock specifically allowed deletion, but with the file gone, we're done with it now.
readStream.Dispose(); 

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

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

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

preventWriteAndDelete.Dispose();
file.MoveTo(destination);
readStream.Dispose();

Ответ 6

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