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

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

Ответ

Ключевое слово async само по себе не создает новый поток. Оно лишь позволяет использовать await внутри метода. Механика выполнения выглядит так:

  1. До первого await: Код выполняется синхронно в вызывающем потоке.
  2. При встрече await: Если ожидаемая задача (Task) еще не завершена, управление возвращается вызывающему коду. Сам поток при этом освобождается.
  3. После завершения асинхронной операции: Оставшаяся часть метода (продолжение, или continuation) планируется на выполнение. Где именно оно выполнится, зависит от контекста синхронизации (SynchronizationContext).

Пример, демонстрирующий смену потоков:

async Task FooAsync()
{
    // Выполняется в исходном потоке (например, UI-потоке).
    Console.WriteLine($"Before await. Thread ID: {Thread.CurrentThread.ManagedThreadId}");

    // Приостановка. UI-поток освобождается, не блокируется.
    await Task.Delay(1000);

    // Продолжение может выполниться в ДРУГОМ потоке (например, из пула).
    Console.WriteLine($"After await. Thread ID: {Thread.CurrentThread.ManagedThreadId}");
}

Важные контексты:

  • UI-приложения (WPF, WinForms): Благодаря SynchronizationContext продолжение обычно выполняется в том же UI-потоке, что обеспечивает безопасный доступ к элементам управления.
  • ASP.NET Core и консольные приложения: Контекст синхронизации по умолчанию отсутствует. Продолжение выполняется в потоке из пула (ThreadPool), который может быть любым.

Итог: Асинхронность — это не про создание потоков, а про эффективное использование существующих за счет освобождения их во время операций ввода-вывода (I/O).