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

141

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

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

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

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

Поэтому, когда я должен использовать threadingи multiprocessingмодули?

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

lucacerone
источник
Там есть еще и Threadмодуль (называемый _threadв Python 3.x). Честно говоря, я никогда не понимал различий сам ...
Незнайка
3
@ Не знаю: как явно сказано в документации Thread/ _thread, это "низкоуровневые примитивы". Вы можете использовать его для создания пользовательских объектов синхронизации, управления порядком соединения дерева потоков и т. Д. Если вы не представляете, зачем вам это нужно, не используйте его и придерживайтесь threading.
abarnert

Ответы:

260

То, что говорит Джулио Франко, верно для многопоточности против многопроцессорности в целом .

Однако в 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библиотеку для написания кода вокруг пула потоков, например:

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

* Это на самом деле не Python, язык, который имеет эту проблему, а CPython, «стандартная» реализация этого языка. Некоторые другие реализации не имеют GIL, например, Jython.

** Если вы используете метод fork start для многопроцессорной обработки, что возможно на большинстве платформ, отличных от Windows, каждый дочерний процесс получает любые ресурсы, которые имел родительский процесс при запуске дочернего процесса, что может быть еще одним способом передачи данных дочерним процессам.

abarnert
источник
спасибо, но я не уверен, что все понял. Во всяком случае, я пытаюсь сделать это немного для целей обучения, и немного потому, что при наивном использовании потоков я вдвое уменьшил скорость своего кода (запуская более 1000 потоков одновременно, каждый из которых вызывает внешнее приложение ... это насыщает процессор, но есть увеличение в 2 раза). Я думаю, что
умное
3
@ LucaCerone: Ах, если ваш код тратит большую часть времени на ожидание во внешних программах, то да, он выиграет от многопоточности. Хорошая точка зрения. Позвольте мне отредактировать ответ, чтобы объяснить это.
abarnert
2
@ LucaCerone: Между тем, какие части вы не понимаете? Не зная уровня знаний, с которого вы начинаете, трудно написать хороший ответ ... но с некоторой обратной связью, возможно, мы сможем придумать что-то полезное для вас и для будущих читателей.
abarnert
3
@ LucaCerone Вы должны прочитать PEP для многопроцессорной обработки здесь . Это дает время и примеры потоков против многопроцессорности.
mr2ert
1
@LucaCerone: если объект, к которому привязан метод, не имеет какого-либо сложного состояния, простейшим способом решения проблемы травления является написание глупой функции-оболочки, которая генерирует объект и вызывает его метод. Если у него действительно сложное состояние, то вам, вероятно, нужно сделать его отмываемым (что довольно легко; pickleдокументы объясняют это), а в худшем случае - ваша глупая оболочка def wrapper(obj, *args): return obj.wrapper(*args).
abarnert
32

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

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

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

Джулио Франко
источник
12
В нем отсутствует очень важная информация о GIL, что вводит его в заблуждение.
abarnert
1
@ mr2ert: Да, это очень важная информация в двух словах. :) Но это немного сложнее, поэтому я написал отдельный ответ.
abarnert
2
Я думал, что прокомментировал, говоря, что @abarnert прав, и я забыл о GIL, отвечая здесь. Так что этот ответ неверен, вы не должны его высказывать.
Джулио Франко
6
Я отклонил этот ответ, потому что он по-прежнему не отвечает вообще, в чем разница между Python threadingи multiprocessing.
Антти Хаапала
Я прочитал, что есть GIL для каждого процесса. Но все ли процессы используют один и тот же интерпретатор python или существует отдельный интерпретатор для потока?
переменная
3

Я считаю, что эта ссылка изящно отвечает на ваш вопрос.

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

ehfaafzv
источник
3

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

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

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

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

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

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

введите описание изображения здесь

Сюжет данных .

Выводы:

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

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

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

    Сравните это с работой с C POSIX, связанной с ЦП, которая достигает ожидаемого 8-кратного ускорения: что означают «реальные», «пользовательские» и «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 МБ / с).

Визуализируйте, какие потоки запущены в данный момент

Этот пост https://rohanvarma.me/GIL/ научил меня, что вы можете запускать обратный вызов всякий раз, когда для потока запланирован target=аргументthreading.Thread и то же самое для multiprocessing.Process.

Это позволяет нам точно видеть, какой поток выполняется в каждый раз. Когда это будет сделано, мы увидим что-то вроде (я составил этот конкретный график):

            +--------------------------------------+
            + Active threads / processes           +
+-----------+--------------------------------------+
|Thread   1 |********     ************             |
|         2 |        *****            *************|
+-----------+--------------------------------------+
|Process  1 |***  ************** ******  ****      |
|         2 |** **** ****** ** ********* **********|
+-----------+--------------------------------------+
            + Time -->                             +
            +--------------------------------------+

который показал бы что:

  • потоки полностью сериализуются GIL
  • процессы могут работать параллельно
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
источник
1

Вот некоторые данные о производительности для Python 2.6.x, которые ставят под сомнение представление о том, что многопоточность является более производительной, чем многопроцессорная обработка в сценариях, связанных с вводом-выводом. Эти результаты получены на 40-процессорной IBM System x3650 M4 BD.

Обработка, связанная с вводом-выводом: пул процессов работал лучше, чем пул потоков

>>> 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')
Марио Агилера
источник
Я использовал ваш код (удалил часть глобуса ) и нашел эти интересные результаты с Python 2.6.6:>>> 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
Алан Гарридо
-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

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

инносам
источник
извини инносам, но мне кажется это C ++? спасибо за ссылки :)
lucacerone
На самом деле идеи многопроцессорности и многопоточности не зависят от языка. Решение будет похоже на приведенный выше код.
Инносам
2
Это не C ++; это псевдокод (или это код для в основном динамически типизированного языка с C-подобным синтаксисом. При этом, я думаю, более полезно написать Python-подобный псевдокод для обучения пользователей Python. (Тем более, что Python-подобный psuedocode часто оказывается исполняемым кодом или, по крайней мере, близким к нему, что редко встречается в C-подобном псевдокоде…)
abarnert
Я переписал его как Python-подобный псевдокод (также используя OO и передавая параметры вместо использования глобальных объектов); не стесняйтесь вернуться, если вы думаете, что делает вещи менее ясными.
abarnert
Также стоит отметить, что в Python stdlib есть встроенная синхронизированная очередь, которая оборачивает все эти детали, а его API потоков и пулов процессов абстрагируют вещи еще дальше. Определенно стоит понять, как работают синхронизированные очереди, но вам редко придется писать их самостоятельно.
abarnert