Почему вашему C# приложению нужны очереди и фоновые задачи?
Представьте: вы запускаете веб-приложение на C#, пользователь нажимает кнопку «Отправить отчёт», и… сервер зависает на 30 секунд, пока генерируется PDF. Знакомо? В 2024 году пользователи не прощают тормозов. Решение — очереди и фоновые задачи. Они превращают синхронный ад в асинхронный рай.
В этой статье мы разберём, как правильно организовывать фоновую обработку в C#: от встроенных Channel<T> до промышленного RabbitMQ. Вы узнаете, как вынести тяжёлую работу из основного потока, не сломав архитектуру. Поехали!
Что такое очередь задач и зачем она в C#?
Очередь (queue) — это структура данных FIFO (First In, First Out). В контексте C# это механизм, который позволяет отложить выполнение задачи, чтобы не блокировать основной поток. Фоновая задача — это код, который выполняется «за кулисами», пока пользователь продолжает кликать по сайту.
Типичные сценарии:
- Отправка email-уведомлений (100 писем — 100 запросов к SMTP)
- Генерация отчётов и Excel-файлов
- Обработка загруженных изображений (ресайз, watermark)
- Интеграция с внешними API (медленные ответы)
- Логирование в базу данных (чтобы не тормозить UI)
Без очередей C# приложение будет «залипать» на каждом тяжёлом запросе. С очередями — вы просто кладёте задачу в буфер и отвечаете пользователю: «Готово, мы обрабатываем».
Вариант 1: Фоновые задачи через BackgroundService (встроенный .NET)
Самый простой способ — использовать BackgroundService из Microsoft.Extensions.Hosting. Это базовый класс для создания фоновых сервисов в ASP.NET Core. Он идеально подходит для простых сценариев, когда не нужна сложная маршрутизация сообщений.
Пример: фоновый обработчик с Channel<T>
Давайте напишем простой Producer-Consumer. Producer (контроллер) кладёт задачу в канал, Consumer (BackgroundService) забирает и выполняет.
// Шаг 1. Создаём канал (потокобезопасную очередь)
public class TaskChannel
{
private readonly Channel<string> _channel =
Channel.CreateBounded<string>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait
});
public ChannelWriter<string> Writer => _channel.Writer;
public ChannelReader<string> Reader => _channel.Reader;
}
// Шаг 2. BackgroundService, который читает из канала
public class EmailBackgroundService : BackgroundService
{
private readonly TaskChannel _taskChannel;
public EmailBackgroundService(TaskChannel taskChannel)
{
_taskChannel = taskChannel;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var emailData in _taskChannel.Reader.ReadAllAsync(stoppingToken))
{
// Имитация отправки письма (2 секунды)
await Task.Delay(2000, stoppingToken);
Console.WriteLine($"Письмо отправлено: {emailData}");
}
}
}
// Шаг 3. Контроллер, который кладёт задачи
[ApiController]
[Route("api/email")]
public class EmailController : ControllerBase
{
private readonly TaskChannel _taskChannel;
public EmailController(TaskChannel taskChannel)
{
_taskChannel = taskChannel;
}
[HttpPost("send")]
public IActionResult SendEmail([FromBody] string emailData)
{
_taskChannel.Writer.TryWrite(emailData);
return Accepted(new { message = "Письмо поставлено в очередь" });
}
}
// Шаг 4. Регистрация в Program.cs
builder.Services.AddSingleton<TaskChannel>();
builder.Services.AddHostedService<EmailBackgroundService>();
Что здесь происходит? Пользователь вызывает POST /api/email/send — данные попадают в канал. EmailBackgroundService в фоне забирает их и обрабатывает. Ответ пользователю приходит мгновенно (HTTP 202 Accepted).
Плюсы и минусы Channel<T>
- Плюсы: встроено в .NET, не требует внешних зависимостей, потокобезопасно, есть ограничение буфера.
- Минусы: данные живут только в памяти (потеряются при перезапуске), не подходит для распределённых систем.
Вариант 2: RabbitMQ — промышленная очередь для C#
Когда одного сервера мало, а данные нужно сохранять между перезапусками, на сцену выходит RabbitMQ. Это брокер сообщений, который работает по протоколу AMQP. В C# для работы с ним используется библиотека RabbitMQ.Client.
RabbitMQ решает проблемы: гарантия доставки, маршрутизация по routing key, работа в кластере. Идеально для микросервисов.
Пример: Producer и Consumer на C#
// Producer — отправляем сообщение в очередь
public class RabbitMqProducer
{
private readonly IConnection _connection;
private readonly IModel _channel;
public RabbitMqProducer(string hostName = "localhost")
{
var factory = new ConnectionFactory() { HostName = hostName };
_connection = factory.CreateConnection();
_channel = _connection.CreateModel();
_channel.QueueDeclare(queue: "task_queue",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
}
public void Publish(string message)
{
var body = Encoding.UTF8.GetBytes(message);
_channel.BasicPublish(exchange: "",
routingKey: "task_queue",
basicProperties: null,
body: body);
Console.WriteLine($"Отправлено: {message}");
}
public void Dispose()
{
_channel?.Close();
_connection?.Close();
}
}
// Consumer — фоновый сервис, который слушает очередь
public class RabbitMqConsumerService : BackgroundService
{
private readonly IConnection _connection;
private readonly IModel _channel;
public RabbitMqConsumerService()
{
var factory = new ConnectionFactory() { HostName = "localhost" };
_connection = factory.CreateConnection();
_channel = _connection.CreateModel();
_channel.QueueDeclare(queue: "task_queue",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
_channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
var consumer = new EventingBasicConsumer(_channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine($"Обрабатываю: {message}");
// Имитация работы
Thread.Sleep(2000);
_channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
Console.WriteLine($"Завершено: {message}");
};
_channel.BasicConsume(queue: "task_queue",
autoAck: false,
consumer: consumer);
return Task.CompletedTask;
}
public override void Dispose()
{
_channel?.Close();
_connection?.Close();
base.Dispose();
}
}
Обратите внимание: BasicQos(prefetchCount: 1) гарантирует, что consumer не получит новое сообщение, пока не подтвердит текущее.