Антипаттерны: async void, .Result, sync over async
↑ 2.3 Асинхронность и многопоточность · ← Предыдущая
Зачем это на собесе
Практически обязательный вопрос: «что не так в этом async-коде». Нужно быстро узнавать deadlock от
.Resultиasync void.
Объяснение
| Антипаттерн | Чем плох | Как правильно |
|---|---|---|
async void | исключения не перехватить, нельзя дождаться, крэш процесса | async Task; async void только для обработчиков событий |
.Result, .Wait(), .GetAwaiter().GetResult() | блокирует поток, риск deadlock | await по всей цепочке |
| sync over async | блокирующий вызов async-метода в sync-коде | сделать вызывающий код асинхронным |
async over sync (Task.Run вокруг синхронного I/O) | иллюзия асинхронности, занимает поток пула | настоящий async API |
| Fire-and-forget без обработки | потерянные исключения | фоновый сервис, Channel, логирование в continuation |
Отсутствие CancellationToken | нельзя отменить, висящие запросы | пробрасывать токен во все async-методы |
await в цикле там, где допустим параллелизм | лишняя последовательность | Task.WhenAll с ограничением |
Task.Run в ASP.NET-обработчике | лишнее переключение, нет выигрыша | вызывать async напрямую |
Deadlock от .Result
// UI или классический ASP.NET с SynchronizationContext
public string Load() => LoadAsync().Result; // deadlock
async Task<string> LoadAsync()
{
await Task.Delay(100); // продолжение хочет вернуться в UI-поток
return "ok"; // а он заблокирован на .Result
}В ASP.NET Core SynchronizationContext нет, поэтому классического deadlock не будет, но останется блокировка потока пула и рост latency (thread starvation).
async void
// Плохо: исключение уронит процесс, дождаться нельзя
async void SaveAsync() { await repo.SaveAsync(); }
// Хорошо
async Task SaveAsync() { await repo.SaveAsync(); }ConfigureAwait(false)
В библиотечном коде ConfigureAwait(false) отвязывает продолжение от контекста и снижает риск deadlock; в ASP.NET Core приложении он не нужен.
Нюансы и подводные камни
asyncбезawait— предупреждение компилятора; метод выполняется синхронно.- Возврат
Taskбезasync(elision) экономит state machine, но меняет обработку исключений и время жизниusing. Task.Run(() => asyncMethod().Wait())— не решение, а ещё один способ занять поток.- Проверьте
asyncлямбды в LINQ (Select(async x => ...)): получаетеIEnumerable<Task>, а не результаты.
Практика
- Воспроизведите deadlock на WinForms/WPF-примере с
.Resultи исправьте. - Найдите в своём проекте
.Result,.Wait()иasync void, замените. - Подключите анализаторы (
Microsoft.VisualStudio.Threading.Analyzers, правила CA2007, CA2012) и посмотрите предупреждения.
Вопросы с ответами
Почему async void опасен?
Нет
Task, поэтому вызывающий не может дождаться завершения или поймать исключение; необработанное исключение уходит в SynchronizationContext и роняет процесс. Допустим только для обработчиков событий.
Почему .Result вызывает deadlock?
Поток удерживает контекст и блокируется, а продолжение метода после
awaitждёт возврата в этот же контекст. Ни один не может продвинуться.
Нужен ли ConfigureAwait(false) в ASP.NET Core?
В самом приложении нет (контекста синхронизации нет), в переиспользуемых библиотеках — желателен.
Как правильно запускать фоновую работу?
BackgroundService/IHostedService, очередь наChannel, а не голый fire-and-forget.