Когда имеет смысл использовать потоки вместо синхронных функций
Что проверяют
Понимаете ли вы, что из-за GIL потоки в CPython ускоряют ожидание, а не вычисления. Следом спрашивают, чем потоки отличаются от asyncio и процессов и как между ними выбирать.
Как отвечать
Потоки оправданы, когда программа большую часть времени ждёт: сеть, диск, база, внешние API. На блокирующем вводе-выводе и в time.sleep интерпретатор отпускает GIL, и пока один поток ждёт ответа, другой работает. Десять HTTP-запросов по секунде каждый в пуле из десяти потоков займут около секунды, а не десять.
Для чистых вычислений на Python потоки ускорения не дадут: байткод в каждый момент исполняет один поток. Здесь нужны процессы (ProcessPoolExecutor, multiprocessing) или библиотеки на C, которые сами отпускают GIL, как NumPy. С Python 3.13 есть отдельная сборка без GIL, в 3.14 она получила статус официально поддерживаемой, но по умолчанию GIL на месте, а часть расширений к такой сборке ещё не готова.
Как выбирать:
- потоки: блокирующие библиотеки без async-версии (
requests,boto3, старые драйверы БД), десятки или сотни одновременных задач, фоновая работа в GUI. Удобнее всегоThreadPoolExecutor, по умолчанию он создаёт доmin(32, ядра + 4)потоков; - asyncio: тысячи одновременных соединений и полностью асинхронный стек. Блокирующий вызов внутри корутины выносят в поток через
asyncio.to_thread(); - процессы: тяжёлые вычисления, которым нужны все ядра.
Где спотыкаются
Считают, что GIL делает код потокобезопасным. counter += 1 состоит из нескольких инструкций байткода, и между ними поток может смениться. Общие данные защищают threading.Lock или передают через queue.Queue.
Создают отдельный Thread на каждую задачу без ограничения. Тысяча задач превращается в тысячу потоков ОС со своими стеками и переключениями контекста; пул решает проблему.
Теряют исключения. В ThreadPoolExecutor ошибка сохраняется в объекте Future и никуда не всплывает, пока вы не вызовете result(). Задачу, запущенную через submit() и забытую, никто не проверит.