Очереди задач в C#: полное руководство для начинающих

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Работа с очередями задач в 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}                

Блоги

Рекомендаций по книгам