Я пытаюсь понять преимущества multiprocessing над threading. Я знаю, что многопроцессорная обработка распространяется вокруг Global Interpreter Lock, но какие другие преимущества есть, и может ли threading не делать то же самое?
Многопроцессорная обработка vs Threading Python
Ответ 1
Модуль threading
использует потоки, модуль multiprocessing
использует процессы. Разница в том, что потоки выполняются в одном и том же пространстве памяти, тогда как процессы имеют отдельную память. Это затрудняет обмен объектами между процессами с многопроцессорной обработкой. Поскольку потоки используют одну и ту же память, необходимо принять меры предосторожности или одновременно записать два потока в одну и ту же память. Для этого используется глобальная блокировка интерпретатора.
Процессы нереста немного медленнее, чем нерестовые нити. После того, как они работают, нет большой разницы.
Ответ 2
Вот некоторые плюсы/минусы, которые я придумал.
Multiprocessing
Pros
- Отдельное пространство памяти
- Код обычно прост.
- Использование нескольких процессоров и ядер
- Предотвращает ограничения GIL для cPython
- Устраняет большинство потребностей для примитивов синхронизации, если только вы не используете разделяемую память (вместо этого она больше представляет собой модель связи для IPC)
- Процессы дочерних процессов прерываются/уничтожаются
- Модуль Python
multiprocessing
содержит полезные абстракции с интерфейсом, похожим наthreading.Thread
- Обязательно с cPython для обработки с привязкой к процессору
Против
- IPC немного сложнее с большим объемом служебных данных (модель связи против разделяемой памяти/объектов)
- Увеличение объема памяти
Threading
Pros
- Легкий - низкий объем памяти
- Общая память - облегчает доступ к состоянию из другого контекста
- Позволяет легко настраивать пользовательские интерфейсы
- Модули расширения cPython C, которые правильно выпускают GIL, будут работать параллельно
- Отличная возможность для приложений, связанных с I/O
Против
- cPython - подчиняется GIL
- Не прерывается/уничтожается
- Если не следовать модели насоса очереди сообщений/сообщений (с использованием модуля
Queue
), то ручное использование примитивов синхронизации становится необходимостью (необходимы решения для детализации блокировки). - Код обычно сложнее понять и получить право - потенциал для условий гонки резко возрастает.
Ответ 3
Задача Threading - позволить приложениям реагировать. Предположим, что у вас есть соединение с базой данных, и вам нужно ответить на ввод пользователя. Без потоковой передачи, если соединение с базой данных занято, приложение не сможет ответить на пользователя. Разделив соединение с базой данных на отдельный поток, вы можете сделать приложение более отзывчивым. Кроме того, поскольку оба потока находятся в одном процессе, они могут обращаться к тем же структурам данных - хорошей производительности и гибкому программному обеспечению.
Обратите внимание, что из-за GIL приложение на самом деле не выполняет сразу две вещи, но мы сделали блокировку ресурсов в базе данных в отдельный поток, чтобы время процессора могло переключаться между ним и взаимодействие с пользователем. Время процессора получает нормировку между потоками.
Многопроцессорность - это время, когда вы действительно хотите, чтобы в любой момент времени было сделано нечто большее, чем одно. Предположим, что вашему приложению необходимо подключиться к 6 базам данных и выполнить сложную матричную трансформацию для каждого набора данных. Помещение каждой работы в отдельный поток может помочь немного, потому что, когда одно соединение простаивает, другой может получить некоторое процессорное время, но обработка не будет выполняться параллельно, потому что GIL означает, что вы только когда-либо используете ресурсы одного процессора, Помещая каждое задание в процесс многопроцессорности, каждый может работать на своем собственном процессоре и работать с полной эффективностью.
Ответ 4
Ключевым преимуществом является изоляция. Процесс сбоя не приведет к снижению других процессов, тогда как сбойный поток, вероятно, приведет к хаосу с другими потоками.
Ответ 5
Еще одна вещь, о которой не упоминалось, заключается в том, что это зависит от того, какую ОС вы используете, где скорость. В Windows процессы являются дорогостоящими, поэтому потоки будут лучше в Windows, но в процессах unix быстрее, чем в их вариантах Windows, поэтому использование процессов в unix намного безопаснее и быстро порождает.
Ответ 6
Цитаты из документации по Python
Я выделил ключевые цитаты документации Python о Process vs Threads и GIL по адресу: Что такое глобальная блокировка интерпретатора (GIL) в CPython?
Процесс против потока экспериментов
Я сделал несколько сравнительных тестов, чтобы показать разницу более конкретно.
В этом тесте я рассчитал время работы процессора и ввода-вывода для различного числа потоков на 8 гиперпотоках CPU. Работа, предоставляемая для каждого потока, всегда одна и та же, так что чем больше потоков, тем больше общего объема работы.
Результаты были:
Выводы:
для работы с привязкой к ЦП многопроцессорная обработка всегда выполняется быстрее, предположительно благодаря GIL
для IO связанной работы. оба имеют одинаковую скорость
потоки только увеличиваются примерно в 4 раза вместо ожидаемых 8x, поскольку я работаю на 8-разрядной машине с гиперпоточностью.
Сравните это с работой с C POSIX, связанной с ЦП, которая достигает ожидаемого 8-кратного ускорения: Что делает & # 39; реальный & # 39 ;, & # 39; пользователь & # 39; и & # 39; sys & # 39; значит в выходных данных времени (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
- процессы могут работать параллельно
Ответ 7
Другие ответы больше сосредоточены на многопоточном и многопроцессорном аспектах, но в python необходимо учитывать глобальный интерпретатор (GIL). Когда создается больше число (например, k) потоков, обычно они не будут увеличивать производительность на k раз, поскольку она все равно будет работать как однопоточное приложение. GIL - это глобальная блокировка, которая блокирует все и позволяет только однопоточное выполнение, использующее только одно ядро. Производительность увеличивается в тех местах, где используются расширения C, такие как numpy, Network, I/O, где выполняется много фоновых работ и выпущен GIL.
Поэтому, когда используется потоковая передача, существует только один поток уровня операционной системы, тогда как python создает псевдо-потоки, которые полностью управляются потоками, но в основном выполняются как один процесс. Преобладание происходит между этими псевдо-потоками. Если процессор работает на максимальной мощности, вы можете переключиться на многопроцессорную обработку.
Теперь в случае автономных экземпляров выполнения вы можете выбрать пул. Но в случае совпадения данных, когда вы можете связываться с процессами, вы должны использовать multiprocessing.Process
.
Ответ 8
Как упоминалось в вопросе, многопроцессорность в Python является единственным реальным способом достижения истинного параллелизма. Многопоточность не может достичь этого, поскольку GIL предотвращает параллельную работу потоков.
Как следствие, потоки не всегда могут быть полезны в Python, и на самом деле может даже привести к ухудшению производительности в зависимости от того, чего вы пытаетесь достичь. Например, если вы выполняете задачу, связанную с процессором, например, распаковывать gzip файлы или 3D-рендеринг (что-то интенсивное в ЦП), то потоки могут фактически помешать вашей работе, а не помогать. В таком случае вы хотели бы использовать Multiprocessing, поскольку только этот метод фактически выполняется параллельно и поможет распределить вес этой задачи. Для этого могут быть некоторые накладные расходы, поскольку Multiprocessing включает в себя копирование памяти сценария в каждый подпроцесс, который может вызвать проблемы для приложений большего размера.
Однако многопоточность становится полезной, когда ваша задача привязана к IO. Например, если большая часть вашей задачи связана с ожиданием вызовов API, вы должны использовать Multithreading, потому что почему бы не запустить другой запрос в другом потоке, пока вы ждете, вместо того, чтобы ваш процессор сидел без дела.
TL; DR
- Многопоточность является параллельной и используется для задач, связанных с IO
- Многопроцессорность достигает истинного параллелизма и используется для задач, связанных с ЦП
Ответ 9
MultiProcessing
- Многопроцессорная обработка добавляет процессоры для увеличения вычислительной мощности.
- Несколько процессов выполняются одновременно.
- Создание процесса отнимает много времени и ресурсов.
- Многопроцессорная обработка может быть симметричной или асимметричной.
- Многопроцессорная библиотека в Python использует отдельное пространство памяти, несколько ядер ЦП, обходит ограничения GIL в CPython, дочерние процессы уничтожаются (например, вызовы функций в программе) и намного проще в использовании.
- Некоторые предостережения модуля требуют большего объема памяти, а IPC немного сложнее и требуют больше ресурсов.
Многопоточность
- Многопоточность создает несколько потоков одного процесса для увеличения вычислительной мощности.
- Несколько потоков одного процесса выполняются одновременно.
- Создание потока экономично как по времени, так и по ресурсам.
- Многопоточная библиотека легка, разделяет память, отвечает за отзывчивый пользовательский интерфейс и хорошо используется для приложений, связанных с вводом/выводом.
- Модуль не подлежит уничтожению и подлежит 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()
Ответ 10
Процесс может иметь несколько потоков. Эти потоки могут совместно использовать память и являются единицами выполнения в процессе.
Процессы выполняются на процессоре, поэтому потоки находятся под каждым процессом. Процессы - это отдельные объекты, которые работают независимо. Если вы хотите обмениваться данными или состоянием между каждым процессом, вы можете использовать инструмент хранения памяти, такой как Cache(redis, memcache)
, Files
или Database
.
Ответ 11
Поскольку я учусь в университете, большинство ответов выше верны. В ПРАКТИКЕ на разных платформах (всегда использующих python) порождение нескольких потоков заканчивается как порождение одного процесса. Разница заключается в том, что нагрузка распределяется между несколькими ядрами, а не только с одним ядром, который обрабатывает все на 100%. Так, если вы создадите, например, 10 потоков на 4-ядерном компьютере, вы получите только 25% мощности процессора! И если у вас появятся 10 процессов, вы получите процессор на 100% (если у вас нет других ограничений). Я не эксперт во всех новых технологиях. Я отвечаю с собственным опытом.