Всегда ли await освобождает поток и возвращает управление асинхронно в C#?
Что проверяют
Понимаете ли вы, что await это не «запуск в другом потоке», а точка, где метод может приостановиться, если результата ещё нет. Уточняющий вопрос обычно про ConfigureAwait(false) и про дедлок от .Result в UI.
Как отвечать
Короткий ответ: нет, не всегда. Компилятор разворачивает await в проверку awaiter.IsCompleted:
- задача уже завершена:
Task.FromResult,Task.CompletedTask,ValueTaskс готовым результатом. Метод просто продолжает работу в том же потоке; - задача не завершена: остаток метода регистрируется как продолжение, метод возвращает вызывающему незавершённую
Task, поток свободен для другой работы; - код до первого
await: выполняется синхронно в потоке вызывающего. Тяжёлый расчёт в началеasync-метода заблокирует UI, сколько быawaitни стояло ниже.
Разведите два вопроса: освобождается ли поток и где продолжится код. ConfigureAwait(false) отвечает только на второй: продолжение не возвращается в захваченный SynchronizationContext и выполняется на потоке пула. На освобождение потока он не влияет.
Если нужно гарантированно уступить поток, есть await Task.Yield(), а в .NET 8 появился ConfigureAwait(ConfigureAwaitOptions.ForceYielding). Во время сетевого I/O поток вообще не занят: ждёт операционная система, а не поток пула.
Сильный штрих: продолжение может выполниться синхронно на потоке, который завершил задачу. Поэтому TaskCompletionSource создают с TaskCreationOptions.RunContinuationsAsynchronously, чтобы чужой код не запустился внутри вашего SetResult.
Где спотыкаются
Утверждают, что без ConfigureAwait(false) поток в UI не освобождается. Освобождается, просто продолжение потом встаёт в очередь UI-потока.
Считают, что async сам по себе делает метод асинхронным. Метод, где все await попадают на завершённые задачи, выполнится целиком синхронно, а async-метод вообще без await компилятор отмечает предупреждением CS1998.
Блокируют асинхронный код через .Result или .Wait() в WinForms, WPF или старом ASP.NET: UI-поток ждёт задачу, а задача ждёт UI-поток для продолжения. В ASP.NET Core контекста синхронизации нет, но блокировка всё равно тратит потоки пула.