Многопроцессорность против многопоточности Python - PullRequest
684 голосов
/ 15 июня 2010

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

Ответы [ 11 ]

747 голосов
/ 15 июня 2010

Вот некоторые плюсы и минусы, которые я придумал.

Многопроцессорная обработка

Плюсы

  • Отдельное пространство памяти
  • Код обычно прост
  • Использует преимущества нескольких процессоров и ядер
  • Позволяет избежать ограничений GIL для cPython
  • Устраняет большинство потребностей примитивов синхронизации, если только вы не используете общую память (вместо этого это скорее коммуникациямодель для IPC)
  • Дочерние процессы прерываются / убиваются
  • Python multiprocessing Модуль включает в себя полезные абстракции с интерфейсом, очень похожим на threading.Thread
  • Обязательно с cPython для CPUобработка с ограничением

Минусы

  • IPC немного сложнее с большей нагрузкой (модель связи по сравнению с разделяемой памятью / объектами)
  • Большой объем памяти

Threading

Плюсы

  • Легкий вес - низкий объем памяти
  • Общая память - упрощает доступ к состоянию из другого контекста
  • Позволяет легко создавать отзывчивые пользовательские интерфейсы
  • Модули расширения cPython C, которые должным образом освобождают GIL, будут работать параллельно
  • Отличный вариант для приложений, связанных с вводом / выводом

Минусы

  • cPython - в зависимости от GIL
  • Не прерываемый / убиваемый
  • Если не следует модели очереди команд / сообщений (используя Queueмодуля), тогда использование ручных примитивов синхронизации становится необходимостью (необходимы решения для детализации блокировки)
  • Код обычно сложнее понять и понять правильно - потенциал условий гонки резко возрастает
608 голосов
/ 15 июня 2010

Модуль threading использует потоки, модуль multiprocessing использует процессы.Разница в том, что потоки работают в одном и том же пространстве памяти, а процессы имеют отдельную память.Это усложняет совместное использование объектов между процессами с многопроцессорностью.Поскольку потоки используют одну и ту же память, необходимо принять меры предосторожности, иначе два потока будут записывать в одну и ту же память одновременно.Вот для чего нужна глобальная блокировка интерпретатора.

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

192 голосов
/ 15 июня 2010

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

Обратите внимание, что благодаря GIL приложение фактически не выполняет две функции одновременно, но мы сделали, поместив блокировку ресурсов базы данных в отдельный поток, чтобы можно было переключать время ЦП между ним и взаимодействием с пользователем.Время процессора распределяется между потоками.

Мультипроцессор - это время, когда вы действительно хотите, чтобы в любой момент времени было выполнено более одного действия.Предположим, что вашему приложению необходимо подключиться к 6 базам данных и выполнить сложное преобразование матрицы для каждого набора данных.Помещение каждой работы в отдельный поток может немного помочь, потому что, когда одно соединение простаивает, другое может получить некоторое время процессора, но обработка не будет выполняться параллельно, потому что GIL означает, что вы когда-либо используете ресурсы только одного процессора,Помещая каждое задание в многопроцессорный процесс, каждое из них может работать на своем собственном процессоре и работать с максимальной эффективностью.

39 голосов
/ 15 июня 2010

Ключевым преимуществом является изоляция.Аварийный процесс не приведет к остановке других процессов, в то время как аварийный поток, вероятно, нанесет ущерб другим потокам.

27 голосов
/ 15 июня 2010

Еще одна вещь, не упомянутая, это то, что это зависит от того, какую операционную систему вы используете, в зависимости от скорости. В Windows процессы являются дорогостоящими, поэтому потоки будут лучше в окнах, но в Unix процессы быстрее, чем их варианты Windows, поэтому использование процессов в Unix намного безопаснее и быстрее.

17 голосов
/ 02 июня 2017

Другие ответы были сосредоточены больше на аспекте многопоточности по сравнению с многопроцессорностью, но в Python необходимо учитывать Global Interpreter Lock ( GIL ). Когда создается большее количество (скажем, k ) потоков, обычно они не увеличивают производительность в k раз, поскольку все равно будут работать как однопоточное приложение. GIL - это глобальная блокировка, которая блокирует все и позволяет выполнять только один поток, используя только одно ядро. Производительность действительно увеличивается в тех местах, где используются такие расширения C, как numpy, Network, I / O, где выполняется много фоновой работы и выпускается GIL.
Таким образом, когда используется threading , существует только один поток уровня операционной системы, в то время как python создает псевдопотоки, которые полностью управляются самим потоком, но по сути работают как один процесс. Происходит вытеснение между этими псевдо-нитями. Если процессор работает на максимальной мощности, вы можете переключиться на многопроцессорность.
Теперь в случае автономных экземпляров выполнения вы можете выбрать пул. Но в случае перекрывающихся данных, когда вы хотите, чтобы процессы взаимодействовали, вы должны использовать multiprocessing.Process.

13 голосов

Цитаты документации Python

Я выделил ключевые цитаты документации Python о Process vs Threads и GIL по адресу: Что такое глобальная блокировка интерпретатора (GIL) в CPython?

Процесс против потоков экспериментов

Я провел небольшой сравнительный анализ, чтобы показать разницу более конкретно.

В этом тесте я рассчитал время работы процессора и ввода-вывода для различного числа потоков на гиперпотоке 8 . Работа, предоставляемая для каждого потока, всегда одна и та же, так что чем больше потоков, тем больше общего объема работы.

Результаты были:

enter image description here

График данных .

Выводы:

  • для работы с привязкой к ЦП многопроцессорная обработка всегда выполняется быстрее, предположительно благодаря GIL

  • для работы, связанной с IO. оба имеют одинаковую скорость

  • потоки масштабируются только до 4x вместо ожидаемых 8x, так как я на машине с 8 нитями.

    Сравните это с работой с C POSIX, связанной с ЦП, которая достигает ожидаемого 8-кратного ускорения: Что означают 'real', 'user' и 'sys' в выходных данных времени (1)?

    TODO: Я не знаю причину этого, должны быть другие неэффективности Python, вступающие в игру.

Тестовый код:

#!/usr/bin/env python3

import multiprocessing
import threading
import time
import sys

def cpu_func(result, niters):
    '''
    A useless CPU bound function.
    '''
    for i in range(niters):
        result = (result * result * i + 2 * result * i * i + 3) % 10000000
    return result

class CpuThread(threading.Thread):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class CpuProcess(multiprocessing.Process):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class IoThread(threading.Thread):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

class IoProcess(multiprocessing.Process):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

if __name__ == '__main__':
    cpu_n_iters = int(sys.argv[1])
    sleep = 1
    cpu_count = multiprocessing.cpu_count()
    input_params = [
        (CpuThread, cpu_n_iters),
        (CpuProcess, cpu_n_iters),
        (IoThread, sleep),
        (IoProcess, sleep),
    ]
    header = ['nthreads']
    for thread_class, _ in input_params:
        header.append(thread_class.__name__)
    print(' '.join(header))
    for nthreads in range(1, 2 * cpu_count):
        results = [nthreads]
        for thread_class, work_size in input_params:
            start_time = time.time()
            threads = []
            for i in range(nthreads):
                thread = thread_class(work_size)
                threads.append(thread)
                thread.start()
            for i, thread in enumerate(threads):
                thread.join()
            results.append(time.time() - start_time)
        print(' '.join('{:.6e}'.format(result) for result in results))

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 МБ / с).

12 голосов
/ 10 сентября 2018

Как уже упоминалось в вопросе, Многопроцессорная обработка в Python является единственным реальным способом достижения истинного параллелизма. Многопоточность не может этого достичь, поскольку GIL препятствует параллельной работе потоков.

Как следствие, многопоточность не всегда может быть полезна в Python, и фактически может даже привести к снижению производительности в зависимости от того, чего вы пытаетесь достичь.Например, если вы выполняете задачу с привязкой к процессору , такую ​​как распаковка gzip-файлов или 3D-рендеринг (все, что сильно загружает процессор), тогда многопоточность может фактически снизить производительность, а не помочь.В таком случае вы захотите использовать Multiprocessing , поскольку только этот метод работает параллельно и поможет распределить вес задачи под рукой.Это может быть связано с некоторыми дополнительными затратами, поскольку Многопроцессорная обработка включает копирование памяти скрипта в каждый подпроцесс, что может вызвать проблемы для приложений большего размера.

Однако Многопоточность становится полезным, когда ваша задача связана с IO .Например, если большая часть вашей задачи связана с ожиданием API-вызовов , вы должны использовать Многопоточность , потому что почему бы не запустить другой запрос в другом потоке, пока вы ожидаете, вместо того, чтобы использовать свой ЦПсидеть сложа руки.

TL; DR

  • Многопоточность является параллельной и используется для IO-bound Задачи
  • Многопроцессорная обеспечивает истинный параллелизм и используется для ЦП Задачи
2 голосов
/ 26 марта 2019

МНОГООБРАБОТКА

  • Многопроцессорная обработка добавляет ЦП для увеличения вычислительной мощности.
  • Одновременно выполняется несколько процессов.
  • Создание процесса выполняетсяотнимает много времени и ресурсов.
  • Многопроцессорная обработка может быть симметричной или асимметричной.
  • Многопроцессорная библиотека в Python использует отдельное пространство памяти, несколько ядер ЦП, обходыОграничения GIL в CPython, дочерние процессы уничтожаются (например, вызовы функций в программе) и намного проще в использовании.
  • Некоторые предостережения модуля занимают больше места в памяти, а IPC немного сложнее и требует больше накладных расходов.

MULTITHREADING

  • Многопоточность создает несколько потоков одного процесса для увеличения вычислительной мощности.
  • Несколько потоководин процесс выполняется одновременно.
  • Создание потока экономично как по времени, так и по ресурсам.
  • Многопоточная библиотека имеет небольшой вес, разделяет память, отвечает за отзывчивый интерфейс и хорошо используется для приложений, связанных с вводом / выводом.
  • Модуль не подлежит уничтожению и подчиняется GIL.
  • Несколько потоков живут в одном и том же процессе в одном и том же пространстве, каждый поток выполняет определенную задачу, имеет свой собственный код, собственную память стека, указатель инструкций и разделяющую память кучи.
  • Если у потока есть утечка памяти, это может повредить другие потоки и родительский процесс.

Пример многопоточности и многопроцессорности с использованием Python

Python 3 имеет возможность Запуск параллельных задач .Это делает нашу работу проще.

Имеется пул потоков и Пул процессов .

Следующее дает понимание:

Пример ThreadPoolExecutor

import concurrent.futures
import urllib.request

URLS = ['http://www.foxnews.com/',
        'http://www.cnn.com/',
        'http://europe.wsj.com/',
        'http://www.bbc.co.uk/',
        'http://some-made-up-domain.com/']

# Retrieve a single page and report the URL and contents
def load_url(url, timeout):
    with urllib.request.urlopen(url, timeout=timeout) as conn:
        return conn.read()

# We can use a with statement to ensure threads are cleaned up promptly
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    # Start the load operations and mark each future with its URL
    future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
    for future in concurrent.futures.as_completed(future_to_url):
        url = future_to_url[future]
        try:
            data = future.result()
        except Exception as exc:
            print('%r generated an exception: %s' % (url, exc))
        else:
            print('%r page is %d bytes' % (url, len(data)))

ProcessPoolExecutor

import concurrent.futures
import math

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419]

def is_prime(n):
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main():
    with concurrent.futures.ProcessPoolExecutor() as executor:
        for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
            print('%d is prime: %s' % (number, prime))

if __name__ == '__main__':
    main()
2 голосов
/ 18 сентября 2017

Процесс может иметь несколько потоков.Эти потоки могут совместно использовать память и являются единицами выполнения в процессе.

Процессы выполняются на ЦП, поэтому потоки находятся под каждым процессом.Процессы - это отдельные объекты, которые работают независимо.Если вы хотите обмениваться данными или состоянием между каждым процессом, вы можете использовать инструмент хранения памяти, такой как Cache(redis, memcache), Files или Database.

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