Урок по треку C# и .NET 8 мин

async/await и Task

Машина состояний, SynchronizationContext, ConfigureAwait, ValueTask, отмена и дедлоки.

Асинхронность это самая частая тема .NET-собесов от middle и выше. Нужно понимать, что await не создаёт потоков, откуда берутся дедлоки и как работает машина состояний.

Зачем async

Пока запрос ждёт базу или сеть, поток ничего не делает. Синхронный код держит поток всё это время, асинхронный отпускает его в пул. Для сервера это означает больше одновременных запросов на тех же потоках. Для UI отзывчивый интерфейс.

public async Task<User?> GetAsync(int id, CancellationToken ct)
{
    var user = await _db.Users.FindAsync(new object[] { id }, ct);
    return user;
}

Что делает await

  1. Получает awaiter задачи. Если задача уже завершена, выполнение идёт дальше синхронно.
  2. Иначе метод запоминает своё состояние, подписывает продолжение и возвращает управление вызывающему (тот получает незавершённую Task).
  3. Когда операция завершится, продолжение планируется: в захваченный SynchronizationContext (UI), иначе на пул потоков.

Метод выполняется синхронно до первого незавершённого await. Для ввода-вывода никакой поток не ждёт: ОС уведомляет о завершении через порты завершения.

Машина состояний

Компилятор превращает async-метод в структуру с методом MoveNext. Локальные переменные, живущие через await, становятся её полями, каждый await это номер состояния. При первом реальном ожидании структура переезжает в кучу.

Task, ValueTask, async void

  • Task/Task<T>: основной вариант.
  • ValueTask<T>: экономит аллокацию, если результат часто готов синхронно. Ждать только один раз.
  • async void: только для обработчиков событий. Его нельзя дождаться, исключение может уронить процесс.

Параллельно, а не последовательно

var a = LoadA();               // обе операции стартовали
var b = LoadB();
await Task.WhenAll(a, b);      // ждём обе

Три await подряд выполняют операции по очереди. Для ограничения параллелизма: Parallel.ForEachAsync с MaxDegreeOfParallelism или SemaphoreSlim.

Дедлок и ConfigureAwait

В UI-приложении (или старом ASP.NET) GetAsync().Result блокирует поток, а продолжение await ждёт этот же поток: взаимная блокировка. Правило: асинхронность до конца, никаких .Result и .Wait().

ConfigureAwait(false) не возвращается в исходный контекст после ожидания. В библиотеках его ставят по-прежнему. В ASP.NET Core контекста синхронизации нет, поэтому классического дедлока не будет, но блокирующее ожидание всё равно ведёт к голоданию пула потоков.

Отмена

Отмена кооперативная: принимайте CancellationToken и передавайте его дальше во все async-вызовы. При отмене бросается OperationCanceledException. Таймаут: CancellationTokenSource.CancelAfter или task.WaitAsync(timeout) (.NET 6).

Task.Run

Нужен, чтобы унести CPU-работу с UI-потока. В ASP.NET Core оборачивать в Task.Run уже асинхронный ввод-вывод бессмысленно: запрос и так на пуле. Фоновую работу делают через BackgroundService и очередь, а не через «выстрелил и забыл».

Исключения

await пробрасывает исключение задачи с исходным стеком. Task.WhenAll хранит все ошибки в AggregateException, await отдаёт первую. Не дождались задачу, и её исключение потерялось.

IAsyncEnumerable

Асинхронный поток данных: async IAsyncEnumerable<T> с yield return, потребление через await foreach. EF Core и ASP.NET Core умеют стримить такие последовательности.

Частые ошибки

  • .Result/.Wait() в асинхронном коде.
  • async void вне обработчиков событий.
  • Последовательные await для независимых операций.
  • Забытый await и потерянные исключения.
  • Принятый, но не переданный дальше CancellationToken.
  • Общий DbContext в параллельных задачах.

Что спросят на собесе

  • Создаёт ли await новый поток? Что происходит на await?
  • Откуда берётся дедлок с .Result и как работает ConfigureAwait(false)?
  • Чем опасен async void?
  • Когда ValueTask лучше Task?
  • Как выполнить запросы параллельно и ограничить их число?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.