Как заблокировать файл и избежать чтения во время записи

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

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

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

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

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

подход с использованием FileShare.Ни один из них не был бы таким более или менее:

    static void Main(string[] args)
    {
        new Thread(new ThreadStart(WriteFile)).Start();
        Thread.Sleep(1000);
        new Thread(new ThreadStart(ReadFile)).Start();

        Console.ReadKey(true);
    }

    static void WriteFile()
    {
        using (FileStream fs = new FileStream("lala.txt", FileMode.Create, FileAccess.Write, FileShare.None))
        using (StreamWriter sw = new StreamWriter(fs))
        {
            Thread.Sleep(3000);
            sw.WriteLine("trolololoooooooooo lolololo");
        }
    }

    static void ReadFile()
    {
        Boolean readed = false;
        Int32 maxTries = 5;

        while (!readed && maxTries > 0)
        {
            try
            {
                Console.WriteLine("Reading...");
                using (FileStream fs = new FileStream("lala.txt", FileMode.Open, FileAccess.Read, FileShare.Read))
                using (StreamReader sr = new StreamReader(fs))
                {
                    while (!sr.EndOfStream)
                        Console.WriteLine(sr.ReadToEnd());
                }
                readed = true;
                Console.WriteLine("Readed");
            }
            catch (IOException)
            {
                Console.WriteLine("Fail: " + maxTries.ToString());
                maxTries--;
                Thread.Sleep(1000);
            }
        }
    }

но мне не нравится тот факт, что мне приходится ловить исключения, пробовать несколько раз и ждать неточного количества времени :|

6 ответов


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

затем он должен будет вращаться, ожидая полного создания файла. Который вы применяете с помощью FileShare.Никто. Улавливание исключений здесь не имеет значения, оно все равно вращается. Нет никакого другого обходного пути для этого в любом случае, если вы P / Invoke.


Я думаю, что правильный подход будет следующим: создание набора строк were u сохранит текущее имя файла таким образом, один поток будет обрабатывать файл одновременно, что-то вроде этого

//somewhere on your code or put on a singleton
static  System.Collections.Generic.HashSet<String> filesAlreadyProcessed= new  System.Collections.Generic.HashSet<String>();


//thread main method code
bool filealreadyprocessed = false
lock(filesAlreadyProcessed){
  if(set.Contains(filename)){
    filealreadyprocessed= true;
  }
  else{
     set.Add(filename)
  }
}
if(!filealreadyprocessed){
//ProcessFile
}

у вас есть способ, чтобы определить, какие файлы создаются?

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

//Request access
ReaderWriterLockSlim fileLock = null;
bool needCreate = false;
lock(Coordination.Instance)
{
    if(Coordination.Instance.ContainsKey(theId))
    {
        fileLock = Coordination.Instance[theId];
    }
    else if(!fileExists(theId)) //check if the file exists at this moment
    {
        Coordination.Instance[theId] = fileLock = new ReaderWriterLockSlim();
        fileLock.EnterWriteLock(); //give no other thread the chance to get into write mode
        needCreate = true;
    }
    else
    {
        //The file exists, and whoever created it, is done with writing. No need to synchronize in this case.
    }
}

if(needCreate)
{
    createFile(theId); //Writes the file from the database
    lock(Coordination.Instance)
        Coordination.Instance.Remove[theId];
    fileLock.ExitWriteLock();
    fileLock = null;
}

if(fileLock != null)
    fileLock.EnterReadLock();

//read your data from the file

if(fileLock != null)
   fileLock.ExitReadLock();

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

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


Ваш вопрос действительно заставил меня задуматься.

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

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

я опубликовал очень простой пример на GitHub.

Не стесняйтесь, чтобы дать ему шанс, и дайте мне знать, что вы думаете.

FYI, если у вас нет git, вы можете использовать svn, чтобы вытащить его http://svn.github.com/statianzo/MultiThreadFileAccessWebApp


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

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

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


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

Я думаю, что мы можем напрямую использовать оператор lock для имени файла следующим образом:

lock(string.Intern("FileLock:absoluteFilePath.txt"))
{
    // your code here
}

Как правило, блокировка строки является плохой идеей из-за интернирования строк. Но в этом конкретном случае он должен гарантировать, что никто другой не сможет получить доступ к этому замку. Просто используйте ту же строку блокировки, прежде чем пытаться читать. Здесь interning работает для нас и не против.

PS: текст 'FileLock' - это просто произвольный текст, чтобы гарантировать, что другие пути к строковым файлам не затронуты.