.NET Разработчик
День 2781. #ЗаметкиНаПолях
Паттерн «Производитель-потребитель» c System.Threading.Channels. Начало
Представим эндпоинт, принимающий файлы, изменяющий их размер и возвращающий ответ. В демо-версии всё работает отлично. Но в проде нагрузка растёт, и эндпойнт получает сотни запросов в секунду. Каждый запрос запускает задачу по изменению размера прямо в потоке обработки; CPU загружается до 100%, а остальные функции приложения начинают завершаться по таймауту, т.к. пул потоков перегружен.
Не обязательно обрабатывать запросы сразу. Достаточно принять данные и обработать их в удобном для системы темпе. Это классическая задача типа «производитель-потребитель»: одна сторона передает задачу, а другая выполняет её со скоростью, которую реально может поддерживать. Для простейшей её реализации не нужны брокеры сообщений, достаточно System.Threading.Channels. Далее рассмотрим реализацию.
Каналы в .NET позволяют передавать данные между производителями и потребителями, работающими параллельно в одном процессе. Производители записывают данные в ChannelWriter<T>, потребители считывают их из ChannelReader<T>, а канал обеспечивает потокобезопасную асинхронную передачу между ними. Важный момент — канал с ограниченным размером (bounded) автоматически обеспечивает механизм «обратного давления» (backpressure).
Почему «наивное» решение только всё усугубляет
Первый порыв в решении проблемы – сделать всё асинхронным: запустить задачу через Task.Run и сразу вернуть ответ:
[HttpPost("process")]
public IActionResult Process(UploadRequest request)
{
// Запустил и забыл. Выглядит асинхронно
_ = Task.Run(() => _imgService.Resize(request));
return Accepted();
}
Такой подход обеспечивает быстрый отклик, поэтому кажется удачным решением. Но это не так. Количество запускаемых задач ничем не ограничено, поэтому всплеск нагрузки, который раньше «вешал» CPU, делает это снова — при этом всем отправляется ответ 202, сигнализирующий об успешном принятии запроса. Если происходит перезапуск процесса, э
23 · 1.6K ·