Многопроцессорность против многопоточности Python
Я пытаюсь понять преимущества многопроцессорности перед многопоточностью. Я знаю, что многопроцессорность позволяет обойти глобальную блокировку интерпретатора, но какие еще есть преимущества, и может ли многопоточность не делать то же самое?
Переведено автоматически
Ответ 1
Вот несколько плюсов / минусов, к которым я пришел.
Многопроцессорность
Плюсы
- Отдельное пространство памяти
- Код обычно прост
- Использует преимущества нескольких процессоров и ядер
- Позволяет избежать ограничений GIL для CPython
- Устраняет большинство потребностей в примитивах синхронизации, если только вы не используете общую память (вместо этого это скорее модель связи для IPC)
- Дочерние процессы можно прерывать / уничтожать
- Модуль Python
multiprocessing
включает полезные абстракции с интерфейсом, очень похожимthreading.Thread
- Необходимо использовать CPython для обработки с привязкой к процессору
Минусы
- IPC немного сложнее с большими накладными расходами (модель связи по сравнению с общей памятью / объектами)
- Больший объем памяти
Многопоточность
Плюсы
- Легкий вес - низкий объем памяти
- Общая память - упрощает доступ к состоянию из другого контекста
- Позволяет легко создавать адаптивные пользовательские интерфейсы
- Модули расширения CPython C, которые должным образом выпускают GIL, будут выполняться параллельно
- Отличный вариант для приложений, связанных с вводом-выводом
Минусы
- CPython - зависит от GIL
- Не прерываемый / уничтожаемый
- Если не следовать модели очереди команд / перекачки сообщений (используя
Queue
модуль), то становится необходимым ручное использование примитивов синхронизации (необходимы решения для детализации блокировки) - Код обычно сложнее понять и получить правильный результат - вероятность возникновения условий гонки резко возрастает
Ответ 2
threading
Модуль использует потоки, multiprocessing
модуль использует процессы. Разница в том, что потоки выполняются в одном и том же пространстве памяти, в то время как процессы имеют отдельную память. Это немного усложняет совместное использование объектов между процессами с многопроцессорной обработкой. Поскольку потоки используют одну и ту же память, необходимо принять меры предосторожности, иначе два потока будут выполнять запись в одну и ту же память одновременно. Для этого и предназначена глобальная блокировка интерпретатора.
Порождение процессов происходит немного медленнее, чем порождение потоков.
Ответ 3
Задача многопоточности - сделать приложения отзывчивыми. Предположим, у вас есть подключение к базе данных и вам нужно реагировать на ввод данных пользователем. Без многопоточности, если подключение к базе данных занято, приложение не сможет отвечать пользователю. Выделив подключение к базе данных в отдельный поток, вы можете сделать приложение более отзывчивым. Кроме того, поскольку оба потока находятся в одном процессе, они могут обращаться к одним и тем же структурам данных - хорошая производительность плюс гибкий дизайн программного обеспечения.
Обратите внимание, что из-за GIL приложение на самом деле не выполняет две вещи одновременно, но то, что мы сделали, это поместили блокировку ресурсов базы данных в отдельный поток, чтобы процессорное время можно было переключать между ним и взаимодействием с пользователем. Процессорное время распределяется между потоками.
Многопроцессорность предназначена для случаев, когда вы действительно хотите, чтобы одновременно выполнялось более одной задачи. Предположим, вашему приложению необходимо подключиться к 6 базам данных и выполнить сложное матричное преобразование для каждого набора данных. Помещение каждого задания в отдельный поток может немного помочь, потому что, когда одно соединение простаивает, другое может получить некоторое время процессора, но обработка не будет выполняться параллельно, потому что GIL означает, что вы всегда используете ресурсы только одного процессора. Помещая каждое задание в многопроцессорный процесс, каждое может выполняться на своем собственном процессоре и с максимальной эффективностью.
Ответ 4
Цитаты из документации Python
Каноническая версия этого ответа теперь находится на дублирующем вопросе: В чем различия между модулями многопоточности и многопроцессорности?
Я выделил ключевые цитаты из документации Python о процессах, потоках и GIL по адресу: Что такое глобальная блокировка интерпретатора (GIL) в CPython?
Эксперименты с процессами и потоками
Я провел небольшой сравнительный анализ, чтобы более конкретно показать разницу.
В тесте я рассчитал время работы процессора и ввода-вывода для различного количества потоков на 8 гиперпоточных процессорах. Объем работы, предоставляемой для каждого потока, всегда одинаков, так что чем больше потоков, тем больше общий объем выполняемой работы.
Результаты были:
Вывод данных на график.
Выводы:
при работе с привязкой к процессору многопроцессорность всегда выполняется быстрее, предположительно, из-за GIL
для работы с привязкой к вводу-выводу. оба имеют одинаковую скорость
потоки масштабируются только примерно в 4 раза вместо ожидаемых 8x, поскольку я использую 8-поточную машину.
Сравните это с работой на C POSIX с привязкой к процессору, которая достигает ожидаемого 8-кратного ускорения: что означают 'real', 'user' и 'sys' в выводе time (1)?
ЗАДАЧА: я не знаю причины этого, должно быть, в игру вступают другие недостатки 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 + построение кода в том же каталоге.
Протестировано на Ubuntu 18.10, Python 3.6.7, в ноутбуке Lenovo ThinkPad P51 с процессором Intel Core i7-7820HQ CPU (4 ядра / 8 потоков), оперативной памятью: 2 Samsung M471A2K43BB1-CRC (2 Samsung 16GiB), 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
- процессы могут выполняться параллельно