Каковы различия между потоковыми и многопроцессорными модулями?
Я учусь, как использовать threading
и multiprocessing
модули в Python для параллельного выполнения определенных операций и ускорения моего кода.
Я нахожу это трудным (возможно, потому, что у меня нет никакого теоретического фона об этом), чтобы понять, в чем разница между a
5 ответов:
что говорит Джулио Франко верно для многопоточности и многопроцессорности в общем.
Однако, Python* есть дополнительная проблема: существует глобальная блокировка интерпретатора, которая предотвращает одновременное выполнение кода Python двумя потоками в одном процессе. Это означает, что если у вас есть 8 ядер и изменить код на использование 8 потоков, он не сможет использовать 800% CPU и работать в 8 раз быстрее; он будет использовать тот же 100% CPU и работать на та же скорость. (На самом деле, он будет работать немного медленнее, потому что есть дополнительные накладные расходы от потоковой передачи, даже если у вас нет общих данных, но пока игнорируйте это.)
есть исключения из этого правила. Если тяжелое вычисление вашего кода на самом деле не происходит в Python, но в некоторой библиотеке с пользовательским кодом C, который выполняет правильную обработку GIL, например в приложении numpy, вы получите ожидаемое преимущество производительности от потоковой обработки. То же самое верно, если тяжелые вычисления выполняются некоторыми подпроцесс, который вы запускаете и ждете.
что более важно, есть случаи, когда это не имеет значения. Например, сетевой сервер тратит большую часть своего времени на чтение пакетов из сети, а приложение GUI тратит большую часть своего времени на ожидание пользовательских событий. Одна из причин использования потоков в сетевом сервере или графическом приложении заключается в том, чтобы позволить вам выполнять длительные "фоновые задачи", не останавливая основной поток от продолжения обслуживания сетевых пакетов или событий графического интерфейса. И это прекрасно работает с Потоков в 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% CPU, и добавление большего количества потоков просто замедляет ее, то вы сталкиваетесь с проблемой 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), процессы гораздо больше дорого создавать и разрушать.
- есть некоторые дополнительные ограничения на процессы, некоторые из которых отличаются на разных платформах. Смотрите руководство по программированию для сведения.
- The
threading
модуль не имеет некоторых функцийmultiprocessing
модуль. (Вы можете использоватьmultiprocessing.dummy
чтобы получить большую часть отсутствующего API поверх потоков, или вы можете использовать модули более высокого уровня, такие какconcurrent.futures
и не беспокоиться о оно.)
* это на самом деле не Python, язык, который имеет эту проблему, но CPython, "стандартная" реализация этого языка. Некоторые другие реализации не имеют GIL, как Jython.
** если вы используете вилки метод запуска для многопроцессорной обработки-который вы можете использовать на большинстве платформ, отличных от Windows-каждый дочерний процесс получает любые ресурсы, которые были у родителя при запуске ребенка, что может быть другим способ передачи данных детям.
несколько потоков могут существовать в одном процессе. Потоки, принадлежащие одному и тому же процессу, имеют одну и ту же область памяти (могут считывать и записывать одни и те же переменные и могут мешать друг другу). Напротив, разные процессы живут в разных областях памяти, и каждый из них имеет свои собственные переменные. Для связи процессы должны использовать другие каналы (файлы, каналы или сокеты).
Если вы хотите распараллелить вычисление, вы, вероятно, собираетесь чтобы нуждаться в многопоточности, потому что вы, вероятно, хотите, чтобы потоки сотрудничали в одной и той же памяти.
говоря о производительности, потоки быстрее создаются и управляются, чем процессы (потому что ОС не нужно выделять целую новую область виртуальной памяти), а межпоточная связь обычно быстрее, чем межпроцессная связь. Но потоки сложнее программировать. Потоки могут мешать друг другу и могут записывать в память друг друга, но это происходит не так всегда очевидно (из-за нескольких факторов, в основном переупорядочения инструкций и кэширования памяти), и поэтому вам понадобятся примитивы синхронизации для управления доступом к вашим переменным.
Я считаю этой ссылке отвечает на ваш вопрос в элегантном виде.
короче говоря, если одна из ваших подзадач должна ждать, пока другая завершится, многопоточность хороша (например, в тяжелых операциях ввода-вывода); напротив, если ваши подзадачи действительно могут произойти одновременно, предлагается многопроцессорная обработка. Однако вы не создадите больше процессов, чем количество ядер.
вот некоторые данные о производительности для Python 2.6.x это вызывает сомнение в том, что потоковая обработка более эффективна, чем многопроцессорная обработка в сценариях с привязкой к IO. Эти результаты получены от 40-процессорной системы IBM x3650 M4 BD.
обработка с привязкой к IO: пул процессов выполняется лучше, чем пул потоков
>>> do_work(50, 300, 'thread','fileio') do_work function took 455.752 ms >>> do_work(50, 300, 'process','fileio') do_work function took 319.279 ms
обработка с привязкой к процессору: пул процессов выполняется лучше, чем пул потоков
>>> do_work(50, 2000, 'thread','square') do_work function took 338.309 ms >>> do_work(50, 2000, 'process','square') do_work function took 287.488 ms
Это не строгие тесты, но они говорят мне, что многопроцессорная обработка не совсем неэффективна по сравнению с потоковой обработкой.
код, используемый в интерактивной консоли python для вышеуказанных тестов
from multiprocessing import Pool from multiprocessing.pool import ThreadPool import time import sys import os from glob import glob text_for_test = str(range(1,100000)) def fileio(i): try : os.remove(glob('./test/test-*')) except : pass f=open('./test/test-'+str(i),'a') f.write(text_for_test) f.close() f=open('./test/test-'+str(i),'r') text = f.read() f.close() def square(i): return i*i def timing(f): def wrap(*args): time1 = time.time() ret = f(*args) time2 = time.time() print '%s function took %0.3f ms' % (f.func_name, (time2-time1)*1000.0) return ret return wrap result = None @timing def do_work(process_count, items, process_type, method) : pool = None if process_type == 'process' : pool = Pool(processes=process_count) else : pool = ThreadPool(processes=process_count) if method == 'square' : multiple_results = [pool.apply_async(square,(a,)) for a in range(1,items)] result = [res.get() for res in multiple_results] else : multiple_results = [pool.apply_async(fileio,(a,)) for a in range(1,items)] result = [res.get() for res in multiple_results] do_work(50, 300, 'thread','fileio') do_work(50, 300, 'process','fileio') do_work(50, 2000, 'thread','square') do_work(50, 2000, 'process','square')
ну, на большинство вопросов отвечает Джулио Франко. Я более подробно остановлюсь на проблеме потребитель-производитель, которая, я полагаю, поставит вас на правильный путь для вашего решения с помощью многопоточного приложения.
fill_count = Semaphore(0) # items produced empty_count = Semaphore(BUFFER_SIZE) # remaining space buffer = Buffer() def producer(fill_count, empty_count, buffer): while True: item = produceItem() empty_count.down(); buffer.push(item) fill_count.up() def consumer(fill_count, empty_count, buffer): while True: fill_count.down() item = buffer.pop() empty_count.up() consume_item(item)
вы можете прочитать больше о примитивах синхронизации из:
http://linux.die.net/man/7/sem_overview http://docs.python.org/2/library/threading.html
псевдокод выше. Я полагаю, вы должны искать проблему производителя-потребителя, чтобы получить больше ссылок.