Упорядочение вызовов асинхронных методов

#c# #asp.net #threadpool

#c# #asp.net #threadpool

Вопрос:

Я работаю над библиотекой классов, которая регистрирует детали аудита веб-приложения в нескольких типах источников данных (файл, xml, база данных) на основе политик, определенных в файле веб-конфигурации.

Мой метод журнала аудита имеет подпись, похожую на эту: public static void LogInfo(User пользователь, модуль модуля, список lst);

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

Поскольку в одном рабочем процессе существует более 700 вызовов этих методов, я подумал о том, чтобы сделать их асинхронными. Я использовал простой метод из класса ThreadPool под названием QueueUserWorkItem

 ThreadPool.QueueUserWorkItem(o => LogInfo(User user, Module module, List<Object> lst) );
  

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

Есть ли способ, которым я могу контролировать порядок вызываемых потоков, используя QueueUserWorkItem?

Ответ №1:

Я не думаю, что вы можете указать порядок при использовании QueueUserWorkItem .

Чтобы запустить ведение журнала параллельно (в некотором фоновом потоке), вы могли бы использовать ConcurrentQueue<T> . Это потокобезопасная коллекция, доступ к которой возможен из нескольких потоков. Вы могли бы создать один рабочий элемент (или поток), который считывает элементы из коллекции и записывает их в файл. Ваше основное приложение добавило бы элементы в коллекцию. Тот факт, что вы добавляете элементы в коллекцию из одного потока, должен гарантировать, что они будут прочитаны в правильном порядке.

Чтобы упростить задачу, вы можете сохранять Action значения в очереди:

 ConcurrentQueue<Action> logOperations = new ConcurrentQueue<Action>();

// To add logging operation from main thread:
logOperations.Add(() => LogInfo(user, module, lst));
  

Фоновая задача может просто брать Action s из очереди и запускать их:

 // Start this when you create the `logOperations` collection
ThreadPool.QueueUserWorkItem(o => {
    Action op;
    // Repeatedly take log operations amp; run them
    while (logOperations.TryDequeue(out op)) op();
});
  

Если вам нужно остановить фоновый процессор (который записывает данные в журнал), вы можете создать CancellationTokenSource , а также завершить while цикл, когда токен отменяется (основным потоком). Это можно проверить с помощью IsCancellationRequested свойства (см. MSDN)

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

1. РЕДАКТИРОВАТЬ Изначально я предлагал использовать BlockingCollection , но в данном случае достаточно использовать ConcurrentQueue . (На самом деле, я даже понял это до того, как увидел ответ Бризио :-))

2. Я буду вызывать метод аудита несколько раз из событий жизненного цикла страницы. На самом деле это также может быть вызвано несколько раз из одного метода. Может ли ConcurrentQueue<T> позаботиться об этом?

3. @Kunal: Да, вы можете добавить несколько действий (путем Add повторного вызова).

4. Вам следует пересмотреть BlockingCollection . ConcurrentQueue.TryDequeue возвращается false при первом опустошении очереди, что приведет к завершению вашего потока. Вы можете написать логику для ожидания элементов и т.д., Или вы можете просто использовать BlockingCollection . ConcurrentQueue.TryDequeue не поддерживает отмену. Для этого вам понадобится BlockingCollection .

5. @Jim: Да, я думаю, вы правы — использование BlockingCollection , вероятно, было лучшей идеей! (вам не нужна блокировка на стороне производителя, но отмена — довольно полезная вещь).

Ответ №2:

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

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