Почему Python называют однопоточным
Что проверяют
Понимаете ли вы, что ограничение живёт не в языке, а в конкретной реализации, и где оно реально мешает. Для дата-инженера это прямой вопрос о том, как вы распараллеливаете обработку.
Как отвечать
Сама формулировка неточная: потоки в Python настоящие, системные. Ограничивает их GIL, глобальная блокировка интерпретатора в CPython: байт-код в каждый момент исполняет только один поток, потому что подсчёт ссылок у объектов не атомарен, и одна общая блокировка оказалась дешевле, чем защищать каждый объект.
Главное следствие: GIL отпускается на вводе-выводе и внутри C-расширений. Отсюда практические выводы:
- задачи, упирающиеся в сеть, диск или базу, потоками ускоряются нормально: пока один поток ждёт ответ, работают остальные;
- numpy, pandas, pyarrow и библиотеки линейной алгебры считают в C-коде с отпущенным GIL, поэтому тяжёлая векторная арифметика параллелится, а цикл по строкам на чистом Python нет;
- вычисления на чистом Python масштабируются процессами:
ProcessPoolExecutor,multiprocessing,joblibза параметромn_jobs, дальше Dask или Spark. Плата это сериализация данных между процессами, поэтому задачи дробят покрупнее; asyncioэто другая история: он честно однопоточный и решает задачу множества ожиданий в одном потоке, а не множества вычислений.
Полезно добавить, что GIL это свойство CPython, а не языка: у альтернативных реализаций его нет, а в самом CPython появилась экспериментальная сборка без него, пока не включённая по умолчанию.
Где спотыкаются
Заявляют, что потоки в Python бесполезны. Для ввода-вывода они работают, и в конвейере загрузки, где сотни запросов к внешнему API, потоки проще и дешевле процессов.
Считают, что GIL делает код потокобезопасным. Он лишь не даёт двум потокам исполнять байт-код одновременно, а составная операция из нескольких шагов прерывается посередине: инкремент счётчика из двух потоков спокойно теряет часть приращений, и блокировка всё равно нужна.
Тянут процессы туда, где выигрыш съедает передача данных. Пересылка большого датафрейма в каждый воркер стоит дороже самого счёта. Сначала проверяют, отпускает ли GIL используемая библиотека, и только потом множат процессы.