Использование потоков для создания дорогостоящего файла и обработки других запросов к этому файлу

#c# #.net #multithreading #threadpool

#c# #.net #многопоточность #threadpool

Вопрос:

Я пишу веб-сервис, который генерирует, кэширует и обслуживает zip-файлы.

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

Базовый сценарий может выглядеть следующим образом

  • поток 1: дайте мне bigfile.zip
  • поток 1: bigfile.zip не существует
  • поток 1: генерация bigfile.zip
  • поток 2: дайте мне bigfile.zip
  • поток 2: поток 1 генерирует bigfile.zip — подождите, пока это закончится
  • поток 1: завершено создание bigfile.zip
  • поток 1: обслуживание bigfile.zip
  • поток 2: обслуживание bigfile.zip

Поэтому я рассматриваю возможность использования a Thread для достижения этой цели и использования Join() для их синхронизации, как только файл будет готов.

Но здесь у меня проблема. Как мне управлять несколькими запросами для нескольких разных файлов? Я думал об использовании a Dictionary<fileId, Thread> для отслеживания их, но тогда как я мог безопасно удалить поток из словаря, когда он завершил свой процесс? Я не вижу никакого способа сделать это, не наложив блокировку на все это, включая сам процесс. Конечно, это, по-видимому, делает всю идею потоковой передачи избыточной в первую очередь.

 lock(_myLocker)
{
    if(!fileThreads.containsKey(fileId))
    {
        Thread myThread = MakeMeAThread();
        fileThreads.add(fileId, myThread);
    }
    fileThreads[fileId].Join();    
    //We have to do the Join inside the lock, this is the only way we know (in a threadsafe manner) that the dictionary definitely contains our key
}
ServeTheFile();
//How do I clean up the no longer required fileThreads[fileId]?
  

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

  • поток 1: дайте мне bigfile.zip
  • поток 1: bigfile.zip не существует
  • поток 1: генерация bigfile.zip
  • поток 2: дайте мне bigfile.zip
  • поток 2: поток 1 генерирует bigfile.zip — подождите, пока это закончится
  • поток 3: у вас есть bigfile.zip ? — Нет, он генерируется
  • поток 1: завершено создание bigfile.zip
  • поток 1: обслуживание bigfile.zip
  • поток 2: обслуживание bigfile.zip
  • поток 4: у вас есть bigfile.zip ? Да, это готово для вас
  • поток 5: у вас есть invalid.zip ? Нет, это недопустимый запрос

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

Комментарии:

1. Почему бы не переместить Join() оператор за пределы блокировки?

2. Заставьте поток удалить себя из словаря, когда это будет сделано. Еще лучше, используйте Task . Таким образом, вы можете прикрепить продолжение.

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

4. Поэтому вместо использования lock используйте Monitor . TryEnter и т. Д. Если вы делаете это в ASP.NET , тогда то, что генерирует и кэширует файлы, вероятно, должно быть службой Windows, а не потоком в ASP.NET контекст.

5. @AcidJunkie, да, клиенту придется опрашивать, пока файл не будет доступен. Поскольку это веб-приложение, метод по умолчанию — просто запросить файл и подождать. Но когда мы добавляем javascript, мы можем использовать ajax для опроса доступности файла.

Ответ №1:

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

 Dictionary<string, object> _fileLocks = new Dictionary<string, object>();
  

Поэтому, когда поступает запрос на создание файла, вы сначала блокируете объект dictionary. Затем вы проверяете, содержит ли он уже объект блокировки. Если нет, добавьте один. В противном случае получите текущий.

 object lockObject;
lock (_fileLocks)
{
    if (_fileLocks.TryGetValue(fileName, out lockObject) == false)
    {
        lockObject = new object();
        _fileLocks.Add(fileName, lockObject);
    }
}
  

Затем заблокируйте объект блокировки и выполните свою работу:

 lock (lockObject)
{
    // check if the file has been created. if not, generate it
    // load and return the file
}
  

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

Комментарии:

1. Не полное решение. Вам нужно добавить проверку, когда элемент удаляется из словаря, или вы попадете между фазами 1 и 2

2. Это решение ничего не удаляет из словаря. Или я неправильно понимаю, на что вы указываете?

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

4. Я указываю на это во второй раз, ваше решение только добавляет блокировки, но никогда не удаляет их. Когда вы удаляете из _fileLocks ? а что произойдет, если вы находитесь непосредственно перед блокировкой (lockObject), и кто-то снимает блокировку с _fileLocks ? Дело не в том, что ваше направление плохое, оно просто неполное.

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