Что происходит с потоком, который отправил запрос к базе данных и ждет ответа?
Что проверяют
Понимаете ли вы, чем синхронное ожидание отличается от асинхронного на уровне потоков, и знаете ли, что ожидание ответа из сети вообще не требует потока. Это вопрос про стоимость блокировки: от ответа зависит, будете ли вы писать код, который упирается в пул потоков под нагрузкой.
Как отвечать
Сначала про синхронный вызов. Поток, вызвавший ExecuteReader, уходит в состояние ожидания: процессорного времени он не потребляет, но занят целиком, вместе со своим стеком и записью в планировщике. Для консольной утилиты это ничего не стоит, для сервера стоит дорого. Каждый одновременный запрос держит свой поток пула, свободные заканчиваются, а новые пул добавляет не мгновенно, с задержкой. Дальше очередь запросов растёт и время ответа скачет, хотя процессор простаивает. Это и называется голоданием пула потоков.
Теперь асинхронный вызов:
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(ct);
await using var reader = await command.ExecuteReaderAsync(ct);На await метод возвращает управление, поток уходит обратно в пул и берёт другую работу. Никакого «потока, который ждёт», в этот момент нет: ожидание живёт на уровне ядра и драйвера, а когда ответ придёт, продолжение метода поставят в очередь пула и подхватит его любой свободный поток, не обязательно тот же самый. Отсюда вывод: асинхронность тут про пропускную способность, а не про скорость одного запроса, быстрее он не станет.
Где спотыкаются
Говорят, что async создаёт поток. Наоборот: асинхронный ввод-вывод потока не занимает, а вот Task.Run вокруг синхронного вызова именно что перекладывает блокировку на поток пула и ничего не решает.
Вызывают .Result или .Wait() поверх асинхронного метода. В приложении с контекстом синхронизации, например в WPF, это классическая взаимная блокировка: продолжение ждёт занятый поток интерфейса, а тот ждёт задачу. На сервере без контекста блокировки нет, зато есть то самое голодание пула.
Не пробрасывают CancellationToken. Отвалившийся клиент и таймаут сами по себе работу на стороне базы не отменяют: без токена соединение продолжает удерживаться, а ответ уже некому отдавать.