Введение: зачем нужны очереди задач в C#?
Представьте, что вы разрабатываете приложение, которое обрабатывает заказы в интернет-магазине. Клиенты отправляют запросы одновременно, и каждый запрос требует времени (например, проверка платежа или отправка email). Если обрабатывать их последовательно, приложение будет тормозить, а пользователи — ждать. Решение — использовать очередь задач.
Очередь задач (Task Queue) — это структура данных, работающая по принципу FIFO (First In, First Out — «первым пришёл — первым ушёл»). В C# (.NET) очереди реализованы в нескольких вариациях: от простой Queue<T> до потокобезопасной ConcurrentQueue<T>. В этой статье мы разберём, как создавать очереди, добавлять и извлекать задачи, а также как использовать их в асинхронном программировании.
Статья рассчитана на новичков, поэтому мы начнём с основ и постепенно перейдём к более сложным примерам.
1. Основы работы с Queue<T> в C#
Класс Queue<T> находится в пространстве имён System.Collections.Generic. Это простая очередь, которая не поддерживает многопоточность — её нужно использовать только в однопоточных сценариях.
Создание очереди и основные операции
Давайте создадим очередь для хранения строковых задач:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Создаём очередь строк
Queue<string> taskQueue = new Queue<string>();
// Добавляем задачи (Enqueue)
taskQueue.Enqueue("Отправить email клиенту");
taskQueue.Enqueue("Проверить статус заказа");
taskQueue.Enqueue("Обновить базу данных");
Console.WriteLine($"Количество задач: {taskQueue.Count}");
// Извлекаем задачу (Dequeue) — удаляет и возвращает первый элемент
string firstTask = taskQueue.Dequeue();
Console.WriteLine($"Выполняем: {firstTask}");
// Просматриваем следующую задачу без удаления (Peek)
string nextTask = taskQueue.Peek();
Console.WriteLine($"Следующая: {nextTask}");
Console.WriteLine($"Осталось задач: {taskQueue.Count}");
}
}
Вывод программы:
Количество задач: 3
Выполняем: Отправить email клиенту
Следующая: Проверить статус заказа
Осталось задач: 2
Важные методы Queue<T>
Enqueue(item)— добавляет элемент в конец очереди.Dequeue()— удаляет и возвращает первый элемент. Если очередь пуста, выбрасывает исключениеInvalidOperationException.Peek()— возвращает первый элемент без удаления.Clear()— очищает очередь.Contains(item)— проверяет, есть ли элемент в очереди.
Важно: Queue<T> не является потокобезопасной. Если несколько потоков одновременно будут добавлять или извлекать элементы, возникнут ошибки. Для многопоточных сценариев используйте ConcurrentQueue<T>.
2. Потокобезопасная очередь: ConcurrentQueue<T>
В реальных приложениях задачи часто добавляются и обрабатываются в разных потоках. Для этого в .NET существует ConcurrentQueue<T> из пространства имён System.Collections.Concurrent.
Пример: Producer-Consumer (Производитель-Потребитель)
Представьте, что один поток (Producer) генерирует задачи, а другой (Consumer) их выполняет.
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
class Program
{
static void Main()
{
ConcurrentQueue<string> queue = new ConcurrentQueue<string>();
// Производитель: добавляет задачи
Task producer = Task.Run(() =>
{
for (int i = 1; i <= 5; i++)
{
string task = $"Задача {i}";
queue.Enqueue(task);
Console.WriteLine($"[Producer] Добавлена: {task}");
Task.Delay(100).Wait(); // Имитация работы
}
});
// Потребитель: извлекает и выполняет задачи
Task consumer = Task.Run(() =>
{
int processed = 0;
while (processed < 5)
{
if (queue.TryDequeue(out string task))
{
Console.WriteLine($"[Consumer] Выполнена: {task}");
processed++;
}
Task.Delay(150).Wait();
}
});
Task.WaitAll(producer, consumer);
Console.WriteLine("Все задачи обработаны.");
}
}
Ключевое отличие: мы используем TryDequeue() вместо Dequeue(). Он возвращает true, если элемент был успешно извлечён, и false, если очередь пуста — это позволяет избежать исключений.
Методы ConcurrentQueue<T>
Enqueue(item)— добавляет элемент.TryDequeue(out T result)— пытается извлечь элемент (без исключений).TryPeek(out T result)— пытается просмотреть первый элемент.Count— количество элементов (приблизительное, т.к. очередь может изменяться).IsEmpty— проверяет, пуста ли очередь.
3. Очередь задач с использованием Channel (каналы)
Начиная с .NET Core 3.0, в C# появился ещё один мощный инструмент — Channel (System.Threading.Channels). Он специально разработан для сценариев Producer-Consumer и предоставляет богатые возможности: ограничение размера, отмена операций, асинхронное чтение и запись.
Пример с Channel
using System;
using System.Threading.Channels;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// Создаём канал с ограничением в 3 элемента
var channel = Channel.CreateBounded<string>(3);
// Producer
var producer = Task.Run(async () =>
{
for (int i = 1; i <= 5; i++)
{
string task = $"Task {i}";
await channel.Writer.WriteAsync(task);
Console.WriteLine($"[Producer] Sent: {task}");
await Task.Delay(100);
}
channel.Writer.Complete(); // Сообщаем, что задач больше не будет
});
// Consumer
var consumer = Task.Run(async ()