Avatar ·

Как и когда правильно использовать async и await в C#? Разбираем на примерах

Всем привет! Я начинающий C# разработчик, и я никак не могу разобраться с async/await. Читал много статей, но на практике всё равно путаюсь. Вот простой пример. У меня есть метод, который загружает данные с веб-сервера и обрабатывает их. Я слышал, что для долгих операций (например, скачивание файла или запрос к БД) нужно использовать async, чтобы не блокировать UI. Но я написал такой код, и он всё равно подвисает: ```csharp public async Task LoadDataAsync() { using (HttpClient client = new HttpClient()) { // Я думал, что await сделает магию var response = await client.GetStringAsync("https://example.com/data"); // Но UI всё равно зависает на время загрузки! return ProcessData(response); } } private void Button_Click(object sender, RoutedEventArgs e) { var result = LoadDataAsync().Result; // Здесь всё виснет textBox.Text = result; } ``` Я заметил несколько проблем: 1. Если я вызываю `async` метод синхронно через `.Result`, программа зависает. 2. Если я просто вызываю `LoadDataAsync()` без await, то метод выполняется, но я не могу получить результат. 3. Я не понимаю, когда нужно писать `async Task`, а когда просто `void`. Объясните, пожалуйста, на пальцах: - В каких методах нужно писать `async`? - Как правильно вызывать асинхронные методы из UI-событий? - Что такое "контекст синхронизации" и почему из-за него всё виснет? Заранее спасибо! Примеры кода приветствуются. <<>>

Основная проблема: смешивание синхронного и асинхронного кода

Вы столкнулись с классической ошибкой начинающих — попыткой получить результат асинхронного метода синхронно через .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:

  1. Никогда не используйте .Result или .Wait() для асинхронных методов — это ведёт к deadlock'у.
  2. Асинхронность "заразна" — если метод вызывает async метод, он сам должен стать async.
  3. Используйте async void только в обработчиках событий.
  4. Для CPU-интенсивных операций используйте Task.Run, а не async/await.
  5. В библиотеках (не 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).