Как и когда правильно использовать async и await в C#? Разбираем на примерах
Основная проблема: смешивание синхронного и асинхронного кода
Вы столкнулись с классической ошибкой начинающих — попыткой получить результат асинхронного метода синхронно через .Result или .Wait(). Это приводит к deadlock'у (взаимной блокировке) в приложениях с контекстом синхронизации (WPF, WinForms, ASP.NET Classic).
Правило №1: async "растёт" вверх по стеку
Если метод использует await, он сам должен быть помечен async и возвращать Task или Task<T>. Исключение — обработчики событий UI (Button_Click), которые могут возвращать async void (но это единственный случай!).
Как исправить ваш пример
Вот правильная реализация загрузки данных без блокировки UI:
// 1. Метод загрузки — всё верно, async Task<string>
public async Task<string> LoadDataAsync()
{
using (HttpClient client = new HttpClient())
{
// await не блокирует поток, а возвращает управление
var response = await client.GetStringAsync("https://example.com/data");
return ProcessData(response); // выполнится после завершения запроса
}
}
// 2. Обработчик клика — async void (только для UI-событий!)
private async void Button_Click(object sender, RoutedEventArgs e)
{
// await здесь "разбудит" UI после завершения
var result = await LoadDataAsync();
textBox.Text = result; // выполняется в UI-потоке
}
Почему это работает?
await client.GetStringAsync()— приостанавливает метод, НО НЕ БЛОКИРУЕТ поток. Поток UI возвращается в систему и может обрабатывать другие события (двигать окно, нажимать кнопки).- Когда запрос завершается, оставшаяся часть метода (включая
ProcessDataи присвоениеtextBox.Text) возобновляется в оригинальном контексте синхронизации (UI-потоке). - Если бы вы использовали
.Result, вы бы заставили UI-поток спать в ожидании, а запрос не мог бы завершиться, потому что ему нужен тот же UI-поток (deadlock).
Когда использовать async void, а когда async Task?
- async void — ТОЛЬКО для обработчиков событий (Button_Click, OnLoad и т.д.). Их нельзя ожидать снаружи, и исключения в них приводят к падению приложения.
- async Task — для всех остальных методов. Их можно ожидать, они возвращают результат, исключения обрабатываются нормально.
- async Task<T> — если метод возвращает значение (string, int, объект).
Что такое контекст синхронизации?
Это механизм, который "помнит", в каком потоке нужно возобновить выполнение после await. В WPF/WinForms это UI-поток. В консольном приложении контекста нет, и код продолжается в любом потоке пула. Чтобы избежать захвата контекста (например, в библиотеках), используйте .ConfigureAwait(false):
var response = await client.GetStringAsync(url).ConfigureAwait(false);
// После этой строки код выполняется в потоке пула, а не UI
Золотые правила async/await:
- Никогда не используйте
.Resultили.Wait()для асинхронных методов — это ведёт к deadlock'у. - Асинхронность "заразна" — если метод вызывает
asyncметод, он сам должен статьasync. - Используйте
async voidтолько в обработчиках событий. - Для CPU-интенсивных операций используйте
Task.Run, а не async/await. - В библиотеках (не UI) используйте
ConfigureAwait(false)для производительности.
Альтернативный взгляд: когда async НЕ нужен
Важно понимать, что async/await не делает код быстрее — он делает его отзывчивым. Если ваша операция — не I/O (скачивание, запись в БД, чтение файла), а вычисления (сортировка массива, рендеринг 3D), async не поможет. Для CPU-bound задач используйте Task.Run:
// Плохо: async не ускорит вычисления
public async Task<int> ComputeAsync()
{
return await Task.Run(() => LongCalculation());
}
// Лучше: явно указываем, что это фоновая работа
public Task<int> ComputeAsync()
{
return Task.Run(() => LongCalculation());
}
Ещё один важный момент: не все "долгие" методы нужно делать async. Если метод выполняется за 50 мс, накладные расходы на async могут быть больше, чем выгода. Измеряйте производительность!
И наконец, в современном C# (начиная с .NET Core) для веб-приложений async обязателен — каждый запрос обрабатывается в потоке пула, и блокировка этого потока снижает пропускную способность сервера. В ASP.NET Core используйте async для всех I/O операций (Entity Framework, HttpClient, FileStream).