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

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

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

Начать курс

Введение: зачем нужны очереди задач в 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 ()                

Блоги

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