То, что говорит Джулио Франко, верно для многопоточности против многопроцессорности в целом .
Однако в 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:
Единственное реальное предостережение в том, что аргументы и возвращаемые значения ваших заданий должны быть легко перестраиваемыми (и не требовать слишком много времени или памяти для перебора), чтобы их можно было использовать в качестве перекрестного процесса. Обычно это не проблема, но иногда это так.
Но что, если ваша работа не может быть автономной? Если вы можете разработать свой код с точки зрения заданий, которые передают сообщения от одного к другому, это все еще довольно легко. Возможно, вам придется использовать threading.Thread
или multiprocessing.Process
вместо того, чтобы полагаться на пулы. И вам придется создавать queue.Queue
или multiprocessing.Queue
объекты явно. (Существует множество других опций - каналы, сокеты, файлы со скоплениями, ... но дело в том, что вы должны сделать что-то вручную, если автоматическая магия исполнителя недостаточна.)
Но что, если вы даже не можете положиться на передачу сообщений? Что делать, если вам нужно две работы, чтобы изменить одну и ту же структуру и увидеть изменения друг друга? В этом случае вам нужно будет выполнить ручную синхронизацию (блокировки, семафоры, условия и т. Д.) И, если вы хотите использовать процессы, явные объекты совместной памяти для загрузки. Это когда многопоточность (или многопроцессорность) становится сложной. Если вы можете избежать этого, прекрасно; если вы не можете, вам нужно будет прочитать больше, чем кто-либо может вставить в SO-ответ.
Из комментария вы хотели узнать, чем отличаются потоки от процессов в Python. Действительно, если вы прочитаете ответ Джулио Франко и мой, и все наши ссылки, это должно охватить все ... но резюме определенно было бы полезно, так что здесь идет:
- Потоки обмениваются данными по умолчанию; процессы нет.
- Как следствие (1), отправка данных между процессами, как правило, требует их сортировки. **
- Как еще одно следствие (1), прямой обмен данными между процессами обычно требует перевода их в низкоуровневые форматы, такие как Value, Array и
ctypes
типы.
- Процессы не подлежат GIL.
- На некоторых платформах (в основном Windows) процессы создания и уничтожения намного дороже.
- Существуют некоторые дополнительные ограничения для процессов, некоторые из которых отличаются на разных платформах. См. Руководство по программированию для деталей.
threading
Модуль не имеет некоторые особенности multiprocessing
модуля. (Вы можете использовать, multiprocessing.dummy
чтобы получить большую часть отсутствующего API поверх потоков, или вы можете использовать модули более высокого уровня, например, concurrent.futures
и не беспокоиться об этом.)
* Это на самом деле не Python, язык, который имеет эту проблему, а CPython, «стандартная» реализация этого языка. Некоторые другие реализации не имеют GIL, например, Jython.
** Если вы используете метод fork start для многопроцессорной обработки, что возможно на большинстве платформ, отличных от Windows, каждый дочерний процесс получает любые ресурсы, которые имел родительский процесс при запуске дочернего процесса, что может быть еще одним способом передачи данных дочерним процессам.
Thread
модуль (называемый_thread
в Python 3.x). Честно говоря, я никогда не понимал различий сам ...