Работа с очередями задач в C#: для начинающих
В современной разработке часто возникает необходимость обрабатывать множество задач, не блокируя основной поток приложения. Представьте себе веб-сервер, который должен одновременно отвечать сотням пользователей, или фоновый сервис, обрабатывающий загрузку файлов. В таких ситуациях на помощь приходит очередь задач (task queue) — одна из фундаментальных структур данных и паттернов программирования.
В этой статье мы на практических примерах разберем, как работают очереди задач в C# (.NET), начиная с базовых классов и заканчивая потокобезопасными решениями для многопоточных приложений.
Что такое очередь задач и зачем она нужна?
Очередь (Queue) — это структура данных, работающая по принципу FIFO (First In, First Out): первый добавленный элемент будет обработан первым. В контексте задач это означает, что мы можем поставить задачу в очередь и обработать её позже, когда освободятся ресурсы.
Основные сценарии использования очередей задач:
- Фоновая обработка (отправка email, генерация отчетов)
- Балансировка нагрузки (распределение задач между рабочими потоками)
- Буферизация запросов (когда система не может обработать все запросы мгновенно)
- Асинхронное логирование (запись логов без замедления основного приложения)
Базовый класс Queue<T> в C#
В .NET существует встроенный класс Queue<T> из пространства имен System.Collections.Generic. Давайте посмотрим на простейший пример:
using System;
using System.Collections.Generic;
public class SimpleQueueExample
{
public static void Main()
{
// Создаем очередь строк
Queue<string> taskQueue = new Queue<string>();
// Добавляем задачи
taskQueue.Enqueue("Отправить email клиенту");
taskQueue.Enqueue("Сгенерировать отчет");
taskQueue.Enqueue("Обновить кэш");
Console.WriteLine($"Количество задач в очереди: {taskQueue.Count}");
// Обрабатываем задачи по порядку
while (taskQueue.Count > 0)
{
string task = taskQueue.Dequeue();
Console.WriteLine($"Обработка: {task}");
}
Console.WriteLine($"Очередь пуста: {taskQueue.Count == 0}");
}
}
Основные методы Queue<T>:
Enqueue(T item)— добавляет элемент в конец очередиDequeue()— извлекает и удаляет элемент из начала очередиPeek()— возвращает первый элемент без его удаленияCount— количество элементов в очереди
Важно: Queue<T> не является потокобезопасным. Если несколько потоков одновременно будут читать и писать в очередь, возникнут ошибки. Для многопоточных сценариев используйте ConcurrentQueue<T>.
Потокобезопасная очередь: ConcurrentQueue<T>
В реальных приложениях очереди задач почти всегда используются в многопоточном контексте. Для этого в .NET существует ConcurrentQueue<T> из пространства имен System.Collections.Concurrent.
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
public class ConcurrentQueueExample
{
public static void Main()
{
ConcurrentQueue<string> taskQueue = new ConcurrentQueue<string>();
// Задача 1: добавляем элементы (Producer)
Task producer = Task.Run(() =>
{
for (int i = 1; i <= 10; i++)
{
taskQueue.Enqueue($"Задача #{i}");
Console.WriteLine($"Добавлена задача #{i}");
Task.Delay(100).Wait(); // Имитация работы
}
});
// Задача 2: обрабатываем элементы (Consumer)
Task consumer = Task.Run(() =>
{
int processedCount = 0;
while (processedCount < 10)
{
if (taskQueue.TryDequeue(out string task))
{
Console.WriteLine($" Обработана: {task}");
processedCount++;
}
Task.Delay(150).Wait();
}
});
Task.WaitAll(producer, consumer);
Console.WriteLine("Все задачи обработаны!");
}
}
Ключевые особенности ConcurrentQueue<T>:
- Использует
TryDequeue(out T result)вместоDequeue()— не вызывает исключение при пустой очереди - Не блокирует потоки при чтении/записи (lock-free алгоритмы)
- Идеально подходит для паттерна Producer-Consumer
Паттерн Producer-Consumer на практике
Паттерн Producer-Consumer (Производитель-Потребитель) — классический архитектурный шаблон, где один или несколько потоков создают задачи (producers), а другие потоки их обрабатывают (consumers). Очередь выступает буфером между ними.
Рассмотрим более реалистичный пример — систему обработки заказов:
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; }
public decimal Amount { get; set; }
public override string ToString() => $"Заказ #{Id} | {CustomerName} | {Amount:C}";
}
public class OrderProcessingSystem
{
private readonly ConcurrentQueue<Order> _orderQueue = new ConcurrentQueue<Order>();
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
private int _orderCounter = 0;
public void Start()
{
// Запускаем 2 производителя
Task producer1 = Task.Run(() => ProduceOrders("Магазин А"));
Task producer2 = Task.Run(() => ProduceOrders("Магазин Б"));
// Запускаем 3 потребителя
Task consumer1 = Task.Run(() => ConsumeOrders("Обработчик 1"));
Task consumer2 = Task.Run(() => ConsumeOrders("Обработчик 2"));
Task consumer3 = Task.Run(() => ConsumeOrders("Обработчик 3"));
// Работаем 5 секунд, затем останавливаемся
Task.Delay(5000).Wait();
_cts.Cancel();
Task.WaitAll(producer1, producer2, consumer1, consumer2, consumer3);
Console.WriteLine("Система остановлена.");
}
private void ProduceOrders(string source)
{
Random rnd = new Random();
while (!_cts.Token.IsCancellationRequested)
{
int id = Interlocked.Increment(ref _orderCounter);
Order order = new Order
{
Id = id,
CustomerName = $"Клиент_{id}