Что такое пул потоков и как он работает
Что проверяют
Понимаете ли вы, откуда берутся потоки под Task.Run и продолжениями await, и почему пул не безразмерный. Следом почти всегда спрашивают про thread pool starvation: что случится, если занять пул блокирующими вызовами.
Как отвечать
Пул потоков в .NET это общий на весь процесс набор рабочих потоков, которые берут задачи из очереди и после выполнения не завершаются, а ждут следующую. Создать поток дорого: вызов в ОС и резерв стека. Короткие задачи выгоднее отдавать уже готовым потокам.
- очереди: одна глобальная и локальные у каждого рабочего потока. Задача, порождённая внутри пула, попадает в локальную очередь, а простаивающий поток умеет забирать работу у соседей (work stealing);
- размер: минимум по умолчанию равен числу логических процессоров. До минимума потоки создаются по требованию сразу, выше него пул добавляет их осторожно, с задержкой, и подбирает число под пропускную способность (hill climbing). Лишние простаивающие потоки со временем завершаются;
- API: низкоуровнево
ThreadPool.QueueUserWorkItem, в прикладном кодеTask.Run, таймеры и продолженияawait. В ASP.NET Core нет контекста синхронизации, поэтому код послеawaitтоже продолжается на потоке пула; - ввод-вывод: пока идёт асинхронный запрос к сети или диску, поток не занят вовсе, он возвращается в пул и обслуживает другие задачи;
- долгая работа:
TaskCreationOptions.LongRunningпросит отдельный поток, чтобы не держать поток пула минутами.
Потоки пула фоновые: процесс не ждёт их завершения при выходе.
Где спотыкаются
Блокируют потоки пула через .Result, .Wait() или Thread.Sleep посреди асинхронного кода. Под нагрузкой все потоки ждут, новые добавляются медленно, и сервис перестаёт отвечать при почти пустом CPU. Поднять ThreadPool.SetMinThreads значит лечить симптом, а не причину.
Путают задачу и поток. Task это единица работы, а не поток: await новый поток не создаёт, а Task.Run не делает код асинхронным, он лишь переносит его на пул.
Говорят, что пул при старте создаёт фиксированное число потоков. В .NET пул растёт и сжимается сам, а границы можно лишь сдвинуть через SetMinThreads и SetMaxThreads.