В Python есть одна проблема: есть глобальная блокировка интерпретатора, которая не позволяет двум потокам в одном и том же процессе одновременно запускать код Python. Это означает, что если у вас 8 ядер и вы измените код на использование 8 потоков, он не сможет использовать 800% ЦП и работать в 8 раз быстрее; он будет использовать тот же 100% процессор и работать с той же скоростью. (На самом деле, он будет работать немного медленнее, потому что при многопоточности возникают дополнительные издержки, даже если у вас нет общих данных, но пока игнорируйте это.)
Есть исключения из этого. Если тяжелые вычисления в вашем коде на самом деле не происходят в Python, но в какой-то библиотеке с пользовательским кодом C, которая выполняет правильную обработку GIL, например, в numpy-приложении, вы получите ожидаемый выигрыш в производительности от потоков. То же самое верно, если тяжелые вычисления выполняются каким-то подпроцессом, который вы запускаете и ждете.
Что еще более важно, есть случаи, когда это не имеет значения. Например, сетевой сервер тратит большую часть своего времени на чтение пакетов из сети, а приложение с графическим интерфейсом тратит большую часть своего времени на ожидание пользовательских событий. Одна из причин использования потоков в сетевом сервере или приложении с графическим интерфейсом - это возможность выполнять длительные «фоновые задачи», не останавливая основной поток от продолжения обслуживания сетевых пакетов или событий графического интерфейса. И это прекрасно работает с потоками Python. (С технической точки зрения это означает, что потоки Python обеспечивают параллелизм, даже если они не обеспечивают параллелизма ядра.)
Но если вы пишете программу с привязкой к процессору на чистом Python, использование большего количества потоков обычно не помогает.
Использование отдельных процессов не имеет таких проблем с GIL, потому что каждый процесс имеет свой отдельный GIL. Конечно, между потоками и процессами у вас все еще есть те же компромиссы, что и в любых других языках - разделять данные между процессами труднее и дороже, чем между потоками, может быть дорого запускать огромное количество процессов или создавать и уничтожать их часто и т. д. Но GIL сильно влияет на баланс между процессами, и это не так, скажем, для C или Java. Таким образом, вы обнаружите, что используете многопроцессорность гораздо чаще в Python, чем в C или Java.
Между тем, философия Python «включенные батареи» приносит некоторые хорошие новости: очень легко писать код, который можно переключать между потоками и процессами с помощью смены одной строки.
Если вы разрабатываете свой код в терминах автономных «заданий», которые ничего не делят с другими заданиями (или основной программой), кроме ввода и вывода, вы можете использовать concurrent.futures библиотеку для написания кода вокруг пула потоков, например:
with
concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: executor.submit(job, argument) executor.map(some_function, collection_of_independent_things)
# ...
Вы даже можете получить результаты этих заданий и передать их на дальнейшие задания, ждать, пока все будет в порядке выполнения или в порядке завершения и т. Д .; прочитайте раздел об Futureобъектах для деталей.
Теперь, если окажется, что ваша программа постоянно использует 100% ЦП, а добавление большего количества потоков просто замедляет ее, то вы столкнулись с проблемой GIL, поэтому вам нужно переключиться на процессы. Все, что вам нужно сделать, это изменить эту первую строку:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor
Единственное реальное предостережение в том, что аргументы и возвращаемые значения ваших заданий должны быть легко перестраиваемыми (и не требовать слишком много времени или памяти для перебора), чтобы их можно было использовать в перекрестном процессе. Обычно это не проблема, но иногда это так.
Но что, если ваша работа не может быть самостоятельной?