Сколько потоков по умолчанию выделяется для обработки очереди в приложении?
Что проверяют
Знаете ли вы, что очередь потоков не создаёт, и помните ли реальные значения по умолчанию у пулов. За этим сразу идёт второй слой: умеете ли выбирать размер пула под задачу, а не наугад.
Как отвечать
Прямой ответ: у queue.Queue потоков нет вовсе, это потокобезопасная структура данных. Её maxsize ограничивает не воркеров, а длину очереди, то есть даёт обратное давление производителям. Потоки создаёт тот, кто очередь разбирает.
Если вопрос про пул, называйте конкретику. У ThreadPoolExecutor без аргументов число воркеров равно min(32, os.cpu_count() + 4), верхняя граница стоит специально, чтобы задачи на вводе-выводе не плодили потоки бесконечно. У ProcessPoolExecutor по умолчанию берётся число процессоров. Тот же ThreadPoolExecutor работает исполнителем по умолчанию в asyncio, когда вы зовёте run_in_executor с None.
Дальше про выбор размера:
- задача упирается во ввод-вывод: потоков имеет смысл заметно больше, чем ядер, но упираться надо не в ядра, а в ограничитель на той стороне;
- задача считает: потоки под GIL не помогут, нужны процессы либо библиотека, отпускающая GIL внутри вызова;
- в контейнере
os.cpu_count()показывает процессоры хоста, а не квоту, честнее смотреть маску доступности черезos.sched_getaffinity.
Где спотыкаются
Думают, что очередь сама раздаёт задачи потокам. Из-за этого забывают про завершение: без task_done повиснет join, а без сигнала на выход воркеры в бесконечном цикле либо не дадут процессу закрыться, либо умрут посреди задачи как демоны.
Наращивают число потоков, чтобы ускорить вычисления. Под GIL байт-код исполняет один поток, и от десяти потоков счёт станет только медленнее из-за борьбы за захват. Экспериментальная сборка без GIL это меняет, но обещать её в проде рано.
Ставят пул шире, чем ресурс за ним. Полсотни потоков к базе с десятком соединений просто переносят очередь из кода в пул соединений, добавляя таймауты и блокировки, а узкое место остаётся тем же.