Как заблокировать файл и избежать чтения во время записи - PullRequest
8 голосов
/ 08 апреля 2010

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

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

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

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

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

Подход с FileShare.None был бы таким более или менее:

    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 ]

3 голосов
/ 08 апреля 2010

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

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

1 голос
/ 09 апреля 2010

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

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

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

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

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

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

1 голос
/ 08 апреля 2010

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

Допустим, каждый из этих файлов соответствует уникальному идентификатору в вашей базе данных. Вы создаете централизованное местоположение (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();

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

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

1 голос
/ 08 апреля 2010

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

//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
}
0 голосов
/ 30 сентября 2016

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

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

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

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

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

0 голосов
/ 09 апреля 2010

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

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

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

...