Я учусь , как использовать threading
и те multiprocessing
модули в Python для выполнения определенных операций параллельно и ускорить свой код.
Я нахожу это трудным (возможно, потому что у меня нет никакого теоретического фона об этом), чтобы понять, в чем разница между threading.Thread()
объектом и объектом multiprocessing.Process()
.
Кроме того, мне не совсем понятно, как создать очередь заданий и чтобы только 4 (например) из них работали параллельно, в то время как другие ожидают освобождения ресурсов перед выполнением.
Я нахожу примеры в документации понятными, но не очень исчерпывающими; как только я пытаюсь немного усложнить ситуацию, я получаю много странных ошибок (например, метод, который не может быть рассортирован и т. д.).
Поэтому, когда я должен использовать threading
и multiprocessing
модули?
Можете ли вы связать меня с некоторыми ресурсами, которые объясняют концепции этих двух модулей и как правильно их использовать для сложных задач?
Thread
модуль (называемый_thread
в Python 3.x). Честно говоря, я никогда не понимал различий сам ...Thread
/_thread
, это "низкоуровневые примитивы". Вы можете использовать его для создания пользовательских объектов синхронизации, управления порядком соединения дерева потоков и т. Д. Если вы не представляете, зачем вам это нужно, не используйте его и придерживайтесьthreading
.Ответы:
То, что говорит Джулио Франко, верно для многопоточности против многопроцессорности в целом .
Однако в 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
библиотеку для написания кода вокруг пула потоков, например:Вы даже можете получить результаты этих заданий и передать их на дальнейшие задания, ждать, пока все будет в порядке выполнения или в порядке завершения и т. Д .; прочитайте раздел об
Future
объектах для деталей.Теперь, если окажется, что ваша программа постоянно использует 100% ЦП, а добавление большего количества потоков просто замедляет ее, то вы столкнулись с проблемой GIL, поэтому вам нужно переключиться на процессы. Все, что вам нужно сделать, это изменить эту первую строку:
Единственное реальное предостережение в том, что аргументы и возвращаемые значения ваших заданий должны быть легко перестраиваемыми (и не требовать слишком много времени или памяти для перебора), чтобы их можно было использовать в качестве перекрестного процесса. Обычно это не проблема, но иногда это так.
Но что, если ваша работа не может быть автономной? Если вы можете разработать свой код с точки зрения заданий, которые передают сообщения от одного к другому, это все еще довольно легко. Возможно, вам придется использовать
threading.Thread
илиmultiprocessing.Process
вместо того, чтобы полагаться на пулы. И вам придется создаватьqueue.Queue
илиmultiprocessing.Queue
объекты явно. (Существует множество других опций - каналы, сокеты, файлы со скоплениями, ... но дело в том, что вы должны сделать что-то вручную, если автоматическая магия исполнителя недостаточна.)Но что, если вы даже не можете положиться на передачу сообщений? Что делать, если вам нужно две работы, чтобы изменить одну и ту же структуру и увидеть изменения друг друга? В этом случае вам нужно будет выполнить ручную синхронизацию (блокировки, семафоры, условия и т. Д.) И, если вы хотите использовать процессы, явные объекты совместной памяти для загрузки. Это когда многопоточность (или многопроцессорность) становится сложной. Если вы можете избежать этого, прекрасно; если вы не можете, вам нужно будет прочитать больше, чем кто-либо может вставить в SO-ответ.
Из комментария вы хотели узнать, чем отличаются потоки от процессов в Python. Действительно, если вы прочитаете ответ Джулио Франко и мой, и все наши ссылки, это должно охватить все ... но резюме определенно было бы полезно, так что здесь идет:
ctypes
типы.threading
Модуль не имеет некоторые особенностиmultiprocessing
модуля. (Вы можете использовать,multiprocessing.dummy
чтобы получить большую часть отсутствующего API поверх потоков, или вы можете использовать модули более высокого уровня, например,concurrent.futures
и не беспокоиться об этом.)* Это на самом деле не Python, язык, который имеет эту проблему, а CPython, «стандартная» реализация этого языка. Некоторые другие реализации не имеют GIL, например, Jython.
** Если вы используете метод fork start для многопроцессорной обработки, что возможно на большинстве платформ, отличных от Windows, каждый дочерний процесс получает любые ресурсы, которые имел родительский процесс при запуске дочернего процесса, что может быть еще одним способом передачи данных дочерним процессам.
источник
pickle
документы объясняют это), а в худшем случае - ваша глупая оболочкаdef wrapper(obj, *args): return obj.wrapper(*args)
.Несколько потоков могут существовать в одном процессе. Потоки, принадлежащие одному и тому же процессу, совместно используют одну и ту же область памяти (могут считывать и записывать в одни и те же переменные и могут мешать друг другу). Напротив, разные процессы живут в разных областях памяти, и каждый из них имеет свои переменные. Для связи процессы должны использовать другие каналы (файлы, каналы или сокеты).
Если вы хотите распараллелить вычисления, вам, вероятно, понадобится многопоточность, потому что вы, вероятно, хотите, чтобы потоки взаимодействовали в одной и той же памяти.
Говоря о производительности, потоки быстрее создаются и управляются, чем процессы (поскольку ОС не требуется выделять целую новую область виртуальной памяти), а межпотоковое взаимодействие обычно быстрее, чем межпроцессное взаимодействие. Но потоки сложнее программировать. Потоки могут мешать друг другу и могут записывать в память друг друга, но способ, которым это происходит, не всегда очевиден (из-за нескольких факторов, главным образом переупорядочения команд и кэширования памяти), и поэтому вам понадобятся примитивы синхронизации для управления доступом к вашим переменным.
источник
threading
иmultiprocessing
.Я считаю, что эта ссылка изящно отвечает на ваш вопрос.
Короче говоря, если одна из ваших подзадач должна ждать завершения другой, многопоточность хороша (например, в тяжелых операциях ввода / вывода); напротив, если ваши подзадачи действительно могут возникать одновременно, рекомендуется многопроцессорная обработка. Однако вы не будете создавать больше процессов, чем количество ядер.
источник
Цитаты по документации Python
Я выделил ключевые цитаты документации Python о Process vs Threads и GIL по адресу: Что такое глобальная блокировка интерпретатора (GIL) в CPython?
Процесс против потока экспериментов
Я сделал несколько сравнительных тестов, чтобы показать разницу более конкретно.
В этом тесте я рассчитал время работы процессора и ввода-вывода для различного числа потоков на 8-ми процессорном процессоре с гиперпотоками . Работа, предоставляемая для каждого потока, всегда одинакова, так что чем больше потоков, тем больше общего объема работы.
Результаты были:
Сюжет данных .
Выводы:
для работы с ограниченным процессором многопроцессорная обработка всегда выполняется быстрее, предположительно благодаря GIL
для IO связанной работы. оба имеют одинаковую скорость
потоки только увеличиваются примерно в 4 раза вместо ожидаемых 8x, так как я на 8-ниточной машине с гиперпоточностью.
Сравните это с работой с C POSIX, связанной с ЦП, которая достигает ожидаемого 8-кратного ускорения: что означают «реальные», «пользовательские» и «sys» в выводе времени (1)?
TODO: Я не знаю причину этого, должны быть другие неэффективности Python, вступающие в игру.
Тестовый код:
GitHub upstream + нанесение кода на тот же каталог .
Протестировано на Ubuntu 18.10, Python 3.6.7, на ноутбуке Lenovo ThinkPad P51 с процессором: Процессор Intel Core i7-7820HQ (4 ядра / 8 потоков), ОЗУ: 2x Samsung M471A2K43BB1-CRC (2x 16 ГБ), SSD: Samsung MZVLB512HAJQ- 000L7 (3000 МБ / с).
Визуализируйте, какие потоки запущены в данный момент
Этот пост https://rohanvarma.me/GIL/ научил меня, что вы можете запускать обратный вызов всякий раз, когда для потока запланирован
target=
аргументthreading.Thread
и то же самое дляmultiprocessing.Process
.Это позволяет нам точно видеть, какой поток выполняется в каждый раз. Когда это будет сделано, мы увидим что-то вроде (я составил этот конкретный график):
который показал бы что:
источник
Вот некоторые данные о производительности для Python 2.6.x, которые ставят под сомнение представление о том, что многопоточность является более производительной, чем многопроцессорная обработка в сценариях, связанных с вводом-выводом. Эти результаты получены на 40-процессорной IBM System x3650 M4 BD.
Обработка, связанная с вводом-выводом: пул процессов работал лучше, чем пул потоков
Обработка с привязкой к процессору: пул процессов работал лучше, чем пул потоков
Это не строгие тесты, но они говорят мне, что многопроцессорность не совсем бесполезна по сравнению с многопоточностью.
Код, используемый в интерактивной консоли Python для вышеуказанных тестов
источник
>>> do_work(50, 300, 'thread', 'fileio') --> 237.557 ms
>>> do_work(50, 300, 'process', 'fileio') --> 323.963 ms
>>> do_work(50, 2000, 'thread', 'square') --> 232.082 ms
>>> do_work(50, 2000, 'process', 'square') --> 282.785 ms
Ну, на большинство вопросов отвечает Джулио Франко. Я более подробно остановлюсь на проблеме «потребитель-производитель», которая, как я полагаю, поможет вам выбрать правильное решение для использования многопоточного приложения.
Вы можете прочитать больше о примитивах синхронизации из:
Псевдокод выше. Я полагаю, вам следует поискать проблему «производитель-потребитель», чтобы получить больше ссылок.
источник