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

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

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

Кроме того, мне не совсем понятно, как создать экземпляр очереди заданий и иметь только 4 (например) из них, работающих параллельно, в то время как другие ждут освобождения ресурсов до их выполнения.

Я нахожу примеры в документации ясными, но не очень исчерпывающими; как только я попытаюсь немного усложнить ситуацию, я получаю много странных ошибок (например, метод, который нельзя мариновать и т.д.).

Итак, когда следует использовать модули threading и multiprocessing?

Можете ли вы связать меня с некоторыми ресурсами, которые объясняют концепции этих двух модулей и как их правильно использовать для сложных задач?

Ответы

Ответ 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, - каждый дочерний процесс получает любые ресурсы, которые родитель имел при запуске ребенка, что может быть другим способом передачи данных детям.

Ответ 2

Несколько потоков могут существовать в одном процессе. Нити, принадлежащие одному и тому же процессу, имеют одну и ту же область памяти (могут считывать и записывать одни и те же переменные и могут мешать друг другу). Напротив, разные процессы живут в разных областях памяти, и каждый из них имеет свои собственные переменные. Для связи процессы должны использовать другие каналы (файлы, трубы или сокеты).

Если вы хотите распараллелить вычисление, вам, вероятно, понадобится многопоточность, потому что вы, вероятно, хотите, чтобы потоки взаимодействовали в одной и той же памяти.

Говоря об эффективности, потоки быстрее создают и управляют, чем процессы (потому что ОС не нужно выделять целую новую область виртуальной памяти), а межпоточная связь обычно быстрее, чем межпроцессная связь. Но потоки сложнее программировать. Потоки могут мешать друг другу и могут записывать друг другу память, но способ, который это происходит, не всегда очевиден (из-за нескольких факторов, в основном переупорядочения команд и кэширования памяти), поэтому вам понадобятся примитивы синхронизации для контроля доступа к вашим переменным.

Ответ 3

Я верю эта ссылка отвечает на ваш вопрос элегантным способом.

Короче говоря, если одна из ваших подзадач должна ждать, пока другой заканчивается, многопоточность хорошая (например, в тяжелых операциях ввода-вывода); напротив, если ваши субаременты могут действительно произойти в одно и то же время, предлагается многопроцессорность. Однако вы не будете создавать больше процессов, чем количество ядер.

Ответ 4

Здесь приведены некоторые данные о производительности для python 2.6.x, который вызывает вопрос о том, что потоки более эффективны, чем многопроцессорность в сценариях с привязкой к IO. Эти результаты взяты из 40-процессорной системы IBM System 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')

Ответ 5

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

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

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