Выгодно ли использовать асинхронность при множественных вызовах метода, обращающегося к базе данных
Что проверяют
Понимаете ли вы, что именно даёт async при работе с базой: освобождение потока на время ожидания, а не ускорение запроса. Почти всегда следом спрашивают, можно ли запустить несколько запросов параллельно через Task.WhenAll.
Как отвечать
Короткий ответ: да, выгодно, но выигрыш в пропускной способности, а не в скорости одного вызова. Пока запрос идёт к базе, поток не простаивает, а возвращается в пул и обслуживает другие запросы. В ASP.NET Core под нагрузкой это разница между стабильной работой и голоданием пула потоков. Цена небольшая: машина состояний и пара аллокаций на каждый await, на фоне сетевого вызова к базе это незаметно.
Дальше разберите сценарии множественных вызовов:
- последовательные
await: каждый запрос ждёт предыдущего, суммарное время как у синхронного кода, но потоки не блокируются; - параллельно через
Task.WhenAll: с однимDbContextв EF Core так нельзя, он не потокобезопасен и броситInvalidOperationExceptionо второй операции на том же контексте. Нужны отдельные контексты черезIDbContextFactory, и каждый займёт своё соединение из пула. ВMicrosoft.Data.SqlClientразмер пула по умолчанию 100 соединений, и массовый параллелизм быстро его исчерпает; - один запрос вместо многих: чаще всего лучший вариант. Если метод зовётся в цикле по списку, это проблема N+1, и её решают выборкой через
WHERE ... IN,Includeили проекцией, а не асинхронностью.
Упомяните CancellationToken: его передают в ToListAsync и другие асинхронные методы, чтобы отменённый HTTP-запрос не держал соединение с базой.
Где спотыкаются
Вызывают асинхронный метод синхронно через .Result или .Wait(). В классическом ASP.NET и в UI-приложениях это риск взаимоблокировки, в ASP.NET Core голодание пула, и все плюсы асинхронности пропадают.
Запускают Task.WhenAll поверх одного DbContext и получают исключение уже в проде, потому что локально запросы случайно не пересеклись.
Думают, что await Task.WhenAll покажет все ошибки. await пробрасывает только первое исключение, остальные лежат в свойстве Exception самой задачи как AggregateException.