В чем разница между выполнением длительных операций в отдельном потоке вручную и через Task при большом количестве ожиданий
Что проверяют
Понимаете ли вы, что при большом числе ожиданий решает не «поток или задача», а то, занят ли поток во время ожидания. Следом почти всегда спрашивают про Task.Run с синхронным вызовом внутри и про голодание пула потоков.
Как отвечать
Разведите два вида работы. Долгий расчёт нагружает процессор, и ему нужен поток. Ожидание сети, диска или базы процессор не нагружает, и поток для него не нужен вовсе.
new Thread: отдельный поток ОС со своим стеком, по умолчанию под него резервируется порядка мегабайта. Пока он ждёт ответа, он просто спит. Десять тысяч одновременных ожиданий означают десять тысяч потоков, память и переключения контекста;Taskсasync/await:Taskэто обещание результата, а не поток. При асинхронном вводе-выводе запрос уходит в ОС (IOCP в Windows, epoll в Linux), метод отдаёт поток обратно, а продолжение запускается на пуле, когда пришёл ответ. Те же десять тысяч ожиданий обслуживает горстка потоков;Task.Run: нужен для CPU-работы, чтобы унести её с UI-потока или распараллелить. Для очень долгой задачи естьTaskCreationOptions.LongRunning, текущая реализация даёт ей отдельный поток и не занимает пул.
Назовите и остальные отличия. Необработанное исключение в Thread роняет процесс, а в Task сохраняется и выбрасывается при await. Отмена у задач штатная, через CancellationToken. Композиция тоже: Task.WhenAll, Task.WhenAny, Task.Delay вместо Thread.Sleep.
Ручной поток оправдан, когда нужно то, чего пул не даёт: STA-поток для COM или WPF, свой приоритет, выделенный цикл на всё время жизни приложения.
Где спотыкаются
Отвечают «Task это облегчённый поток». Из этого следует ошибка, будто асинхронное ожидание тоже держит поток.
Оборачивают синхронный вызов в Task.Run и считают код асинхронным, или вызывают .Result и .Wait() на асинхронном методе. Потоки пула блокируются, пул добавляет новые не сразу, и под нагрузкой сервис встаёт. В приложении с UI это ещё и классический дедлок.
Запускают бесконечный цикл обработки через обычный Task.Run без LongRunning, навсегда занимая поток пула.