Как реализовать сервис с единственным API методом для обработки тысячи одновременных запросов: использовать асинхронность или потоки
Что проверяют
Различаете ли вы работу, которая ждёт (сеть, база), и работу, которая считает, и понимаете ли, как ASP.NET Core распоряжается пулом потоков. Следом обычно спрашивают, что будет, если внутри метода вызвать .Result, или где у такого сервиса реальное узкое место.
Как отвечать
Правильный ответ начинается с уточнения: что делает этот метод.
- ввод-вывод (запрос в базу, вызов другого API): только
async/awaitпо всей цепочке, от контроллера до драйвера. Пока запрос ждёт ответа, поток возвращается в пул и обслуживает другие запросы. Тысяча одновременных ожиданий держится на десятке потоков; - блокирующий вариант: каждый ожидающий запрос занимает поток целиком. Пул добавляет потоки постепенно, очередь растёт, задержки скачут. Это и есть thread pool starvation;
- вычисления на CPU: асинхронность их не ускорит, предел задаёт число ядер. Здесь честнее принять запрос, положить задачу в
Channel<T>, разбирать её фиксированным числом воркеров вBackgroundService, а клиенту вернуть202 Accepted; - ограничители:
SemaphoreSlimна обращения к медленной зависимости, встроенный rate limiter (AddRateLimiter, появился в .NET 7) на входе,IHttpClientFactoryвместоnew HttpClient()на каждый вызов.
Отдельно назовите зависимости. Пул соединений к базе в SqlClient и Npgsql по умолчанию рассчитан на 100 соединений, и тысяча запросов упрётся в него раньше, чем в потоки. То же с внешним API, у которого свой лимит.
Про Task.WhenAll: он нужен, когда один запрос сам делает несколько независимых вызовов. Принимать входящие запросы параллельно так не нужно, это делает Kestrel.
Где спотыкаются
Говорят «async быстрее». Отдельный запрос он не ускоряет, он повышает пропускную способность за счёт того, что не держит поток во время ожидания.
Смешивают синхронный и асинхронный код: .Result и .Wait() в обработчике возвращают ту же блокировку потоков, от которой уходили.
Оборачивают вызов базы в Task.Run внутри контроллера. Запрос и так идёт на потоке пула, это лишнее переключение без выигрыша, а для ввода-вывода есть нормальные асинхронные методы драйвера.