Каковы различия между потоковыми и многопроцессорными модулями?


Я учусь, как использовать threading и multiprocessing модули в Python для параллельного выполнения определенных операций и ускорения моего кода.

Я нахожу это трудным (возможно, потому, что у меня нет никакого теоретического фона об этом), чтобы понять, в чем разница между a

5 99

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. потоки общий доступ к данным по умолчанию; процессы - нет.
  2. как следствие (1), отправка данных между процессами обычно требует травления и распаковки его.**
  3. как еще одно следствие (1), прямой обмен данными между процессами обычно требует помещения его в низкоуровневые форматы, такие как Value, Array и ctypes типы.
  4. процессы не подчиняются GIL.
  5. на некоторых платформах (в основном Windows), процессы гораздо больше дорого создавать и разрушать.
  6. есть некоторые дополнительные ограничения на процессы, некоторые из которых отличаются на разных платформах. Смотрите руководство по программированию для сведения.
  7. 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

псевдокод выше. Я полагаю, вы должны искать проблему производителя-потребителя, чтобы получить больше ссылок.