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

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

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

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

Я читал о 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);
            }
        }
    }

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


person vtortola    schedule 08.04.2010    source источник
comment
Это FileShare.None вместо FileAccess.None (FileAccess определяет доступ, который имеет ваше приложение, а FileShare используется для блокировки файла, как вы хотите)   -  person jpabluz    schedule 08.04.2010
comment
Что касается другой темы, не забудьте разблокировать файл всякий раз, когда ваше приложение уничтожается, я ненавижу, когда после этого блокировка остается.   -  person jpabluz    schedule 08.04.2010
comment
Я отредактировал и исправил, спасибо!   -  person vtortola    schedule 08.04.2010


Ответы (6)


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

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

person Hans Passant    schedule 08.04.2010
comment
Похоже, вы правы, другого обходного пути для этого нет. Спасибо! - person vtortola; 08.04.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
}
person Sebastian Marcet    schedule 08.04.2010
comment
В том то и проблема, что я не хочу блокировать общий элемент для всех файлов. Во-первых, получить блокировку дорого, и я не хочу получать блокировку для каждого вызова, запрашивающего файл, независимо от того, существует ли файл уже или нет. Во-вторых, я не хочу блокировать потоки, которые пытаются получить другой файл, потому что я создаю один из них. По этим причинам я хочу заблокировать сам файл. Ваше здоровье. - person vtortola; 08.04.2010
comment
Измеряли ли вы время получения блокировки и блокировки до завершения по сравнению со временем пробуждения потока, проверки доступа, получения исключения, перехода в спящий режим и повторения несколько раз? Я ожидаю, что стратегия блокировки будет более желательной здесь. Thread.Sleep менее желателен для блокировки по замку. Что, если поток записи завершится раньше? Читаемый поток не просыпается. Возможно, вы захотите использовать ManualResetEvent для управления доступом между двумя потоками. - person Paul Williams; 10.05.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, безусловно, не идеальна, но если вашему приложению требуется глобальная синхронизация, то это способ добиться этого.

person Christian Klauser    schedule 08.04.2010
comment
Та же проблема, что и в коде @hworangdo: в каждом запросе вы должны получить блокировку, даже если она вам не нужна. - person vtortola; 08.04.2010
comment
@vtortola: Ага. В защиту моего ответа: получение блокировки не дорого (измерьте, это действительно ничего, особенно по сравнению с файловым вводом-выводом), но ожидание того, что другой поток освободит блокировку, стоит дорого. Вы можете попытаться найти реализацию словаря без блокировки. Вам нужно быть осторожным только в том случае, если файл необходимо создать, чтобы только один поток получил задание на его создание. - person Christian Klauser; 08.04.2010
comment
Вы, вероятно, правы, я никогда не проверял, насколько дорого приобретение замка, я знаю это, потому что я читал это. Ожидание снятия блокировки другим потоком, безусловно, дороже, но это произойдет только один раз для каждого файла. Я проверю ваш подход позже, может быть, ваш быстрее. Спасибо! - person vtortola; 08.04.2010

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

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

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

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

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

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

person statenjason    schedule 08.04.2010

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

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

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

person Clive Saha    schedule 09.04.2010

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

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

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

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

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

person Benaiah    schedule 30.09.2016