Как работает синхронность и что происходит с потоками во время ожидания в async/await?
Что проверяют
Понимаете ли вы, что при ожидании ввода-вывода поток не занят вовсе, и знаете ли, кто выполняет продолжение.
Как отвечать
Синхронный вызов занимает поток на всё время работы, включая ожидание сети или диска. Поток стоит в блокировке, но всё равно занят: он не может обслуживать другие запросы, а его стек висит в памяти.
При await над операцией ввода-вывода происходит другое. Компилятор превратил метод в конечный автомат: дойдя до await, он подписывается на завершение задачи и возвращает управление вызывающему. Поток возвращается в пул и берёт следующую работу.
Пока операция выполняется, потока нет вообще. Ждёт не поток, а драйвер устройства и порт завершения ввода-вывода в ядре. Это ключевая мысль ответа: часто говорят «поток ждёт в фоне», и это неверно.
Когда результат готов, продолжение (остаток метода) ставится в очередь на выполнение. Где оно выполнится, решает контекст синхронизации: в ASP.NET Core его нет, поэтому продолжение подхватывает любой поток пула; в WPF и WinForms контекст есть, и продолжение вернётся в поток интерфейса, что и нужно для обновления элементов.
Отсюда две практики: ConfigureAwait(false) в библиотечном коде, чтобы не требовать возврата в исходный контекст, и запрет на .Result и .Wait(), которые блокируют поток в ожидании продолжения, а продолжению нужен как раз этот контекст, отсюда классический дедлок.
Где спотыкаются
Считают, что во время ожидания задачу держит фоновый поток.
Оборачивают запрос к базе в Task.Run, снова занимая поток.
Смешивают асинхронный код с блокирующим ожиданием.
Не могут объяснить, зачем нужен ConfigureAwait(false).