Почему после синхронной операции выполнение кода может продолжаться в другом потоке
Что проверяют
Понимаете ли вы, что поток и выполнение кода в C# это разные вещи, и что после await продолжение метода планируется заново. По ответу видно, писали ли вы асинхронный код руками или знаете про async только то, что он «делает быстрее».
Как отвечать
Начните с того, что async сам по себе не создаёт поток. await разрезает метод на части: компилятор строит конечный автомат, и всё, что идёт после точки ожидания, становится продолжением, которое кто-то должен запустить.
Куда попадёт это продолжение, решает захваченный контекст:
- если
SynchronizationContext.Currentне пуст (WPF, WinForms, MAUI), продолжение вернётся в поток интерфейса; - если контекста нет (консоль, ASP.NET Core, библиотека), продолжение выполнит поток пула, и это почти наверняка не тот поток, что был до
await; ConfigureAwait(false)означает явный отказ от возврата в контекст.
// до await поток интерфейса, после него поток пула
var body = await http.GetStringAsync(url).ConfigureAwait(false);Скажите и про случай, когда задача к моменту ожидания уже завершена: тогда await отрабатывает синхронно и поток не меняется. Именно поэтому код, случайно работавший на тестах, ломается на первом настоящем ожидании, и в библиотеках принято ставить ConfigureAwait(false), а не надеяться на поток.
Где спотыкаются
Считают, что async занимает поток на всё время операции. Настоящий асинхронный ввод-вывод потока не держит вовсе: завершения ждёт операционная система, а продолжение ставится в очередь по её сигналу. Поток тратит только Task.Run, то есть перенос счётной работы в пул.
Второе: .Result и .Wait(). Там, где контекст есть, это классический дедлок: продолжение ждёт освобождения потока интерфейса, а тот ждёт задачу. В ASP.NET Core дедлока нет, зато пул выедается заблокированными потоками, и сервис встаёт под нагрузкой.
Третье: полагаются на привязку к потоку. Поле с атрибутом ThreadStatic, захват lock вокруг ожидания, обращение к элементам интерфейса после await без возврата в контекст. Состояние, которое должно пережить ожидание, держат в AsyncLocal, а не в потоке.