Ответ 1
Что говорит Джулио Франко, верно для многопоточности и многопроцессорности вообще.
Однако Python * имеет дополнительную проблему: существует глобальная блокировка Interpreter, которая предотвращает одновременный запуск двух потоков в одном и том же процессе кода Python. Это означает, что если у вас есть 8 ядер и смените код на 8 потоков, он не сможет использовать 800% процессор и работать быстрее на 8 раз; он будет использовать тот же 100% процессор и работать с одинаковой скоростью. (В действительности, он будет работать немного медленнее, потому что дополнительные потоки из потоковой передачи, даже если у вас нет общих данных, но на этот раз игнорируйте.)
Есть исключения из этого. Если в Python на самом деле не выполняется тяжелое вычисление кода, но в какой-то библиотеке с пользовательским кодом C, который выполняет правильную обработку GIL, например, с помощью приложения numpy, вы получите ожидаемое преимущество в производительности от потоковой передачи. То же самое верно, если тяжелое вычисление выполняется с помощью некоторого подпроцесса, который вы запускаете и ожидаете.
Что еще более важно, бывают случаи, когда это не имеет значения. Например, сетевой сервер тратит большую часть времени на чтение пакетов с сети, а приложение GUI тратит большую часть своего времени на ожидание пользовательских событий. Одна из причин использования потоков в сетевом сервере или графическом приложении - это позволить вам выполнять длительные "фоновые задачи", не останавливая основной поток от продолжения обслуживания сетевых пакетов или событий графического интерфейса. И это прекрасно работает с потоками Python. (В технических терминах это означает, что потоки Python предоставляют вам concurrency, хотя они не дают вам core- parallelism.)
Но если вы пишете программу с привязкой к процессору в чистом 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), прямое совместное использование данных между процессами обычно требует ввода его в низкоуровневые форматы, такие как типы значений, массива и
ctypes
. - Процессы не подлежат GIL.
- На некоторых платформах (в основном Windows) процессы намного дороже создавать и уничтожать.
- Существуют некоторые дополнительные ограничения на процессы, некоторые из которых различны на разных платформах. Подробнее см. Рекомендации по программированию.
- Модуль
threading
не имеет некоторых функций модуляmultiprocessing
. (Вы можете использоватьmultiprocessing.dummy
, чтобы получить большую часть отсутствующего API поверх потоков, или вы можете использовать модули более высокого уровня, такие какconcurrent.futures
и не беспокоиться об этом.)
* Это не на самом деле Python, язык, который имеет эту проблему, но CPython, "стандартную" реализацию этого языка. Некоторые другие реализации не имеют GIL, например Jython.
** Если вы используете метод запуска fork для многопроцессорности, который вы можете использовать на большинстве платформ, отличных от Windows, - каждый дочерний процесс получает любые ресурсы, которые родитель имел при запуске ребенка, что может быть другим способом передачи данных детям.