Антипаттерны: async void, .Result, sync over async

↑ 2.3 Асинхронность и многопоточность · ← Предыдущая

Обязательно~4 мин чтенияУровень: middle

Зачем это на собесе

Практически обязательный вопрос: «что не так в этом async-коде». Нужно быстро узнавать deadlock от .Result и async void.

Объяснение

АнтипаттернЧем плохКак правильно
async voidисключения не перехватить, нельзя дождаться, крэш процессаasync Task; async void только для обработчиков событий
.Result, .Wait(), .GetAwaiter().GetResult()блокирует поток, риск deadlockawait по всей цепочке
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>, а не результаты.

Практика

  1. Воспроизведите deadlock на WinForms/WPF-примере с .Result и исправьте.
  2. Найдите в своём проекте .Result, .Wait() и async void, замените.
  3. Подключите анализаторы (Microsoft.VisualStudio.Threading.Analyzers, правила CA2007, CA2012) и посмотрите предупреждения.

Вопросы с ответами

Связанные темы