Как продолжается выполнение кода после получения ответа от асинхронного запроса в C#?

«Как продолжается выполнение кода после получения ответа от асинхронного запроса в C#?» — вопрос из категории C# Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Выполнение продолжается в методе, который вызвал await, после того как ожидаемая задача (Task или Task<T>) завершится. Механизм основан на конечных автоматах, генерируемых компилятором.

Что происходит под капотом:

  1. При встрече ключевого слова await выполнение текущего метода приостанавливается, если задача еще не завершена.
  2. Управление возвращается вызывающему методу (позволяя потоку не блокироваться).
  3. Когда задача завершается (например, приходит ответ от HTTP-запроса), выполнение исходного метода возобновляется с точки останова.
  4. Контекст синхронизации (например, UI-поток в WPF/WinForms) сохраняется и используется для продолжения по умолчанию, если не указано иное с ConfigureAwait(false).

Практический пример:

public async Task<string> FetchDataAndProcessAsync()
{
    Console.WriteLine("1. Начинаем запрос...");
    // Выполнение приостанавливается здесь, пока запрос не завершится.
    // Поток освобождается для другой работы.
    string data = await httpClient.GetStringAsync("https://api.example.com/data");

    // ! Выполнение продолжится здесь ТОЛЬКО после завершения GetStringAsync.
    // Для UI-приложения это будет тот же UI-поток.
    // Для консольного/веб-приложения это может быть любой поток из пула.
    Console.WriteLine("2. Данные получены, начинаем обработку...");

    string processedData = Process(data); // Синхронная обработка.
    Console.WriteLine("3. Обработка завершена.");

    return processedData;
}

// Вызывающий код
public async Task MainLogic()
{
    var task = FetchDataAndProcessAsync(); // Задача запущена.
    Console.WriteLine("В то время как запрос выполняется, можно делать другую работу...");
    DoOtherWork(); // Эта работа выполняется ПАРАЛЛЕЛЬНО с HTTP-запросом.

    // Ожидаем результат, если он нужен.
    string result = await task; // Здесь также может быть пауза, если задача не завершена.
    Console.WriteLine("4. Результат получен в основном потоке логики: " + result);
}

Критически важные моменты:

  • Не блокируйте асинхронный код: Не используйте .Result или .Wait() на задачах, особенно в UI-потоках или ASP.NET Core контексте — это может привести к взаимоблокировке (deadlock).
  • Используйте ConfigureAwait(false): В библиотечном коде, чтобы избежать привязки к конкретному контексту синхронизации и улучшить производительность.
  • Исключения: Исключения, выброшенные в асинхронной операции, "пробрасываются" в точку await при возобновлении метода.