Многопроцессорность против многопоточности против asyncio в Python 3.4
Я обнаружил, что в Python 3.4 есть несколько разных библиотек для многопроцессорной обработки / многопоточности: многопроцессорная обработка против многопоточности против asyncio.
Но я не знаю, какой из них использовать, или это "рекомендуемый". Они делают то же самое, или разные? Если да, какой из них используется для чего? Я хочу написать программу, которая использует многоядерные на моем компьютере. Но я не знаю, какую библиотеку мне следует изучать.
10 ответов
Они предназначены для (немного) разных целей и / или требований. CPython (типичная, основная реализация Python) по-прежнему имеет глобальную блокировку интерпретатора, поэтому многопоточное приложение (стандартный способ реализации параллельной обработки в настоящее время) является неоптимальным. Вот почему multiprocessing
может быть предпочтительнее, чем threading
, Но не каждая проблема может быть эффективно разделена на [почти независимые] части, поэтому может возникнуть необходимость в интенсивном межпроцессном взаимодействии. Вот почему multiprocessing
не может быть предпочтительнее, чем threading
в общем.
asyncio
(этот метод доступен не только в Python, другие языки и / или фреймворки также имеют его, например, Boost.ASIO) - это метод для эффективной обработки множества операций ввода-вывода из множества одновременных источников без необходимости параллельного выполнения кода, Так что это просто решение (действительно хорошее!) Для конкретной задачи, а не для параллельной обработки в целом.
[Быстрый ответ]
TL; DR
Делать правильный выбор:
Мы прошли через самые популярные формы параллелизма. Но остается вопрос - когда выбрать, какой? Это действительно зависит от вариантов использования. Исходя из своего опыта (и чтения), я склонен следовать этому псевдокоду:
if io_bound:
if io_very_slow:
print("Use Asyncio")
else:
print("Use Threads")
else:
print("Multi Processing")
- CPU Bound => Мультиобработка
- Связанный ввод / вывод, быстрый ввод / вывод, ограниченное количество соединений => многопоточность
- Связанный ввод / вывод, медленный ввод / вывод, много соединений => Asyncio
[ ПРИМЕЧАНИЕ ]:
- Если у вас есть длинный метод вызова (то есть метод, содержащий время ожидания), лучшим выбором будет
asyncio
или жеtwisted
или жеtornado
подход (сопрограммные методы), который работает с одним потоком как параллелизм. asyncio
работает на Python3.- Uvloop очень быстро
asyncio
цикл обработки событий ( uvloop делаетasyncio
В 2-4 раза быстрее.)
В многопроцессорном режиме вы используете несколько процессоров для распределения вычислений. Поскольку каждый из процессоров работает параллельно, вы эффективно можете выполнять несколько задач одновременно. Вы хотели бы использовать многопроцессорность для задач, связанных с процессором. Примером может быть попытка вычислить сумму всех элементов огромного списка. Если ваша машина имеет 8 ядер, вы можете "разрезать" список на 8 меньших списков и вычислить сумму каждого из этих списков отдельно для отдельного ядра, а затем просто сложить эти числа. Вы получите ~8-кратное ускорение, сделав это.
В потоке вам не нужно несколько процессоров. Представьте себе программу, которая отправляет множество HTTP-запросов в Интернет. Если вы использовали однопоточную программу, она останавливала выполнение (блок) при каждом запросе, ожидала ответа, а затем продолжала после получения ответа. Проблема здесь в том, что ваш процессор на самом деле не работает, ожидая, пока какой-то внешний сервер выполнит эту работу; Тем временем он действительно мог бы проделать некоторую полезную работу! Исправление заключается в использовании потоков - вы можете создать множество из них, каждая из которых отвечает за запрос некоторого контента из Интернета. Прелесть потоков состоит в том, что даже если они работают на одном ЦП, ЦП время от времени "замораживает" выполнение одного потока и переходит к выполнению другого (это называется переключением контекста, и это происходит постоянно при недетерминированном интервалы).Итак, если ваша задача связана с вводом-выводом - использовать многопоточность.
asyncio, по сути, распределяет потоки, когда не процессор, а вы, как программист (или собственно ваше приложение), решаете, где и когда происходит переключение контекста. В Python вы используетеawait
ключевое слово, чтобы приостановить выполнение вашей сопрограммы (определяется с помощью async
ключевое слово).
Это основная идея:
Это IO- BOUND? ---------> ИСПОЛЬЗОВАНИЕ
asyncio
ЭТО ЦП - ТЯЖЕЛЫЙ? -----> ИСПОЛЬЗОВАТЬ
multiprocessing
ЕЩЕ? ----------------------> ИСПОЛЬЗОВАНИЕ
threading
Поэтому в основном придерживайтесь потоковой передачи, если у вас нет проблем с вводом-выводом / процессором.
Многие ответы подсказывают, как выбрать только 1 вариант, но почему нельзя использовать все 3? В этом ответе я объясню, как вы можете использовать для управления объединением всех трех форм параллелизма , а также легко переключаться между ними позже, если это необходимо.
Краткий ответ
Многие разработчики, которые впервые используют параллелизм в Python, в конечном итоге будут использовать
Эти высокоуровневые API предоставляются через
Поскольку они используют унифицированные интерфейсы, вы также обнаружите, что этот код использует или часто будет использовать . не является исключением из этого и предоставляет способ использовать его с помощью следующего кода:
import asyncio
from concurrent.futures import Executor
from functools import partial
from typing import Any, Callable, Optional, TypeVar
T = TypeVar("T")
async def run_in_executor(
executor: Optional[Executor],
func: Callable[..., T],
/,
*args: Any,
**kwargs: Any,
) -> T:
"""
Run `func(*args, **kwargs)` asynchronously, using an executor.
If the executor is None, use the default ThreadPoolExecutor.
"""
return await asyncio.get_running_loop().run_in_executor(
executor,
partial(func, *args, **kwargs),
)
# Example usage for running `print` in a thread.
async def main():
await run_in_executor(None, print, "O" * 100_000)
asyncio.run(main())
На самом деле оказалось, что использование with было настолько распространено, что в Python 3.9 они добавили
Длинный ответ
Есть ли недостатки у этого подхода?
Да. С , самым большим недостатком является то, что асинхронные функции — это не то же самое, что синхронные функции. Это может сбить с толку новых пользователей и привести к большому количеству переделок, если вы не начали программировать с самого начала.
Другим недостатком является то, что пользователи вашего кода также будут вынуждены использовать файлы . Все эти необходимые доработки часто оставляют у начинающих пользователей действительно кислый привкус во рту.
Есть ли в этом какие-либо преимущества, не связанные с производительностью?
Да. Подобно тому, как использование выгодно перед
Есть ли в этом какие-то преимущества в производительности?
Да и нет. В конечном итоге все зависит от задачи. В некоторых случаях это может не помочь (хотя, скорее всего, и не навредит), а в других случаях может сильно помочь. Остальная часть этого ответа дает некоторые объяснения того, почему использование для запуска
- Объединение нескольких исполнителей и другого асинхронного кода
по существу обеспечивает значительно больший контроль над параллелизмом за счет того , что вам нужно больше контролировать параллелизм. Если вы хотите одновременно запустить какой-то код, используя вместе с другим кодом, используя , это не так просто сделать с помощью синхронного кода, но очень просто с .
import asyncio
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
async def with_processing():
with ProcessPoolExecutor() as executor:
tasks = [...]
for task in asyncio.as_completed(tasks):
result = await task
...
async def with_threading():
with ThreadPoolExecutor() as executor:
tasks = [...]
for task in asyncio.as_completed(tasks):
result = await task
...
async def main():
await asyncio.gather(with_processing(), with_threading())
asyncio.run(main())
Как это работает? По существу просит исполнителей выполнять свои функции. Затем, пока работает исполнитель, будет выполняться другой код. Например,
- Сужение в том, какие участки кода нуждаются в исполнителях
Нечасто у вас будет много исполнителей в вашем коде, но распространенная проблема, которую я видел, когда люди используют потоки/процессы, заключается в том, что они будут запихивать весь свой код в поток/процесс, ожидая, что он сработает. . Например, однажды я увидел такой код (примерно):
from concurrent.futures import ThreadPoolExecutor
import requests
def get_data(url):
return requests.get(url).json()["data"]
urls = [...]
with ThreadPoolExecutor() as executor:
for data in executor.map(get_data, urls):
print(data)
Самое забавное в этом фрагменте кода то, что с параллелизмом он работал медленнее, чем без него. Почему? Потому что результат был большим, а наличие большого количества потоков, потребляющих огромное количество памяти, было катастрофическим . К счастью, решение было простым:
from concurrent.futures import ThreadPoolExecutor
import requests
urls = [...]
with ThreadPoolExecutor() as executor:
for response in executor.map(requests.get, urls):
print(response.json()["data"])
Теперь только один
Урок здесь?
Вы не должны пытаться просто разбить весь свой код на потоки/процессы, вместо этого вы должны сосредоточиться на том, какая часть кода действительно нуждается в параллелизме.
Но что, если
import asyncio
import requests
async def get_data(url):
# A lot of code.
...
# The specific part that needs threading.
response = await asyncio.to_thread(requests.get, url, some_other_params)
# A lot of code.
...
return data
urls = [...]
async def main():
tasks = [get_data(url) for url in urls]
for task in asyncio.as_completed(tasks):
data = await task
print(data)
asyncio.run(main())
Попытка сделать то же самое с
Я не профессиональный пользователь Python, но, как студент компьютерной архитектуры, я думаю, что могу поделиться некоторыми своими соображениями при выборе между многопроцессорной обработкой и многопоточностью. Кроме того, в некоторых других ответах (даже среди тех, которые получили более высокие голоса) неправильно используется техническая терминология, поэтому я думаю, что также необходимо сделать некоторые разъяснения по ним, и я сделаю это в первую очередь.
Фундаментальное различие между многопроцессорностью и многопоточностью заключается в том, используют ли они одно и то же пространство памяти. Потоки имеют общий доступ к одному и тому же пространству виртуальной памяти, поэтому потоки могут эффективно и легко обмениваться результатами своих вычислений (нулевое копирование и выполнение полностью в пользовательском пространстве).
С другой стороны, процессы имеют отдельные области виртуальной памяти. Они не могут напрямую читать или записывать пространство памяти другого процесса, точно так же, как человек не может читать или изменять мысли другого человека, не разговаривая с ним. (Разрешение этого было бы нарушением защиты памяти и лишало бы цели использование виртуальной памяти.) Для обмена данными между процессами они должны полагаться на средства операционной системы (например, передачу сообщений), и по нескольким причинам это более важно. дороже, чем схема «разделяемой памяти», используемая потоками. Одна из причин заключается в том, что для вызова механизма передачи сообщений ОС требуется системный вызов, который переключает выполнение кода из пользовательского режима в режим ядра, что занимает много времени;
Неправильно говорить, что многопоточная программа может использовать только один ЦП. Причина, по которой многие так говорят, связана с артефактом реализации CPython: глобальная блокировка интерпретатора (GIL). Из-за GIL потоки в процессе CPython сериализуются. В результате получается, что многопоточная программа на Python использует только один ЦП.
Но многопоточные компьютерные программы в целом не ограничиваются одним ядром, и для Python реализации, не использующие GIL, действительно могут запускать множество потоков параллельно, то есть работать на нескольких процессорах одновременно. (См. https://wiki.python.org/moin/GlobalInterpreterLock).
Учитывая, что CPython является преобладающей реализацией Python, понятно, почему многопоточные программы на Python обычно приравниваются к привязке к одному ядру.
В Python с GIL единственный способ раскрыть всю мощь многоядерности — это использовать многопроцессорность (есть исключения из этого, как указано ниже). Но вашу проблему лучше легко разделить на параллельные подзадачи, которые имеют минимальную взаимосвязь, в противном случае потребуется много межпроцессного взаимодействия, и, как объяснялось выше, накладные расходы на использование механизма передачи сообщений ОС будут дорогостоящими. , иногда такие дорогостоящие, что преимущества параллельной обработки полностью нивелируются. Если характер вашей проблемы требует интенсивного взаимодействия между параллельными подпрограммами, многопоточность — естественный путь. К сожалению, с CPython истинная, эффективная параллельная многопоточность невозможна из-за GIL. В этом случае вы должны понимать, что Python не является оптимальным инструментом для вашего проекта, и рассмотреть возможность использования другого языка.
Есть одно альтернативное решение — реализовать подпрограммы параллельной обработки во внешней библиотеке, написанной на C (или других языках), и импортировать этот модуль в Python. CPython GIL не будет блокировать потоки, порожденные этой внешней библиотекой.
Итак, при бремени GIL многопоточность в CPython хороша? Однако, как уже упоминалось в других ответах, он по-прежнему предлагает преимущества, если вы выполняете ввод-вывод или сетевую связь. В этих случаях соответствующие вычисления выполняются не вашим ЦП, а другими устройствами (в случае ввода-вывода контроллер диска и контроллер DMA (прямой доступ к памяти) будут передавать данные с минимальным участием ЦП; в случае сети, сетевая карта (сетевая карта) и DMA позаботятся о большей части задачи без участия ЦП), поэтому, как только поток делегирует такую задачу сетевому адаптеру или контроллеру диска, ОС может перевести этот поток в спящее состояние и переключиться на другой. потоки одной и той же программы для выполнения полезной работы.
В моем понимании модуль asyncio — это по сути частный случай многопоточности для операций ввода-вывода.
Итак: программы с интенсивным использованием ЦП, которые можно легко разделить для запуска в нескольких процессах с ограниченным взаимодействием: используйте многопоточность, если GIL не существует (например, Jython), или используйте многопроцессорность, если GIL присутствует (например, CPython).
Программы с интенсивным использованием ЦП, которые требуют интенсивного взаимодействия между параллельными подпрограммами: используйте многопоточность, если GIL не существует, или используйте другой язык программирования.
Много IO: asyncio
Уже много хороших ответов. Не могу подробнее рассказать о том, когда использовать каждый из них. Это более интересная комбинация двух. Многопроцессорность + asyncio: https://pypi.org/project/aiomultiprocess/.
Сценарий использования, для которого он был разработан, был highio, но при этом использовалось столько же доступных ядер. Facebook использовал эту библиотеку для написания своего рода файлового сервера на основе Python. Asyncio допускает трафик, связанный с вводом-выводом, но многопроцессорность позволяет создавать несколько циклов событий и потоков на нескольких ядрах.
Ex-код из репо:
import asyncio
from aiohttp import request
from aiomultiprocess import Pool
async def get(url):
async with request("GET", url) as response:
return await response.text("utf-8")
async def main():
urls = ["https://jreese.sh", ...]
async with Pool() as pool:
async for result in pool.map(get, urls):
... # process result
if __name__ == '__main__':
# Python 3.7
asyncio.run(main())
# Python 3.6
# loop = asyncio.get_event_loop()
# loop.run_until_complete(main())
Многопроцессорность может выполняться параллельно .
Многопоточность и асинхронность не могут выполняться параллельно .
С процессором Intel(R) Core(TM) i7-8700K с тактовой частотой 3,70 ГГц и 32,0 ГБ ОЗУ я подсчитал, сколько простых чисел находится между2
и100000
с 2 процессами , 2 потоками и 2 асинхронными задачами , как показано ниже. * Это вычисление с привязкой к процессору :
Поскольку многопроцессорность может выполняться параллельно , то многопроцессорность в два раза быстрее, чем многопоточность и асинхронность , как показано выше.
Я использовал 3 набора кода ниже:
Многопроцессорность:
# "process_test.py"
from multiprocessing import Process
import time
start_time = time.time()
def test():
num = 100000
primes = 0
for i in range(2, num + 1):
for j in range(2, i):
if i % j == 0:
break
else:
primes += 1
print(primes)
if __name__ == "__main__": # This is needed to run processes on Windows
process_list = []
for _ in range(0, 2): # 2 processes
process = Process(target=test)
process_list.append(process)
for process in process_list:
process.start()
for process in process_list:
process.join()
print(round((time.time() - start_time), 2), "seconds") # 23.87 seconds
Результат:
...
9592
9592
23.87 seconds
Многопоточность:
# "thread_test.py"
from threading import Thread
import time
start_time = time.time()
def test():
num = 100000
primes = 0
for i in range(2, num + 1):
for j in range(2, i):
if i % j == 0:
break
else:
primes += 1
print(primes)
thread_list = []
for _ in range(0, 2): # 2 threads
thread = Thread(target=test)
thread_list.append(thread)
for thread in thread_list:
thread.start()
for thread in thread_list:
thread.join()
print(round((time.time() - start_time), 2), "seconds") # 45.24 seconds
Результат:
...
9592
9592
45.24 seconds
Асинцио:
# "asyncio_test.py"
import asyncio
import time
start_time = time.time()
async def test():
num = 100000
primes = 0
for i in range(2, num + 1):
for j in range(2, i):
if i % j == 0:
break
else:
primes += 1
print(primes)
async def call_tests():
tasks = []
for _ in range(0, 2): # 2 asyncio tasks
tasks.append(test())
await asyncio.gather(*tasks)
asyncio.run(call_tests())
print(round((time.time() - start_time), 2), "seconds") # 44.77 seconds
Результат:
...
9592
9592
44.77 seconds
Многопроцессорность Каждый процесс имеет собственный интерпретатор Python и может работать на отдельном ядре процессора. Многопроцессорность Python — это пакет, поддерживающий создание процессов с использованием API, аналогичного модулю потоковой обработки. Многопроцессорный пакет предлагает истинный параллелизм, эффективно обходя глобальную блокировку интерпретатора за счет использования подпроцессов вместо потоков.
Используйте многопроцессорность при выполнении задач с интенсивным использованием ЦП.
Многопоточность Многопоточность Python позволяет создавать несколько потоков внутри процесса. Эти потоки могут совместно использовать одну и ту же память и ресурсы процесса. В CPython из-за глобальной блокировки интерпретатора в любой момент времени может работать только один поток, поэтому вы не можете использовать несколько ядер. Многопоточность в Python не обеспечивает настоящего параллелизма из-за ограничений GIL.
AsyncioAsyncio работает над концепцией совместной многозадачности. Задачи Asyncio выполняются в одном потоке, поэтому параллелизм отсутствует, но он обеспечивает лучший контроль для разработчика, а не для ОС, как в случае многопоточности.
По этой ссылке есть хорошая дискуссия о преимуществах asyncio над потоками.
есть хороший блог Лей Мао о параллелизме Python.Здесь
Просто другая точка зрения
Существует разница в природе параллелизма в многопоточности и асинхронности. Потоки могут чередоваться в любой точке выполнения. ОС контролирует, когда один поток выбрасывается, а другой получает шанс (выделенный процессор). Нет согласованности и предсказуемости в том, когда потоки будут чередоваться. Вот почему у вас могут быть условия гонки в многопоточности. Однако asyncio является синхронным, пока вы чего-то не ожидаете. Цикл событий будет продолжать выполняться до тех пор, пока не произойдетawait
Вы можете ясно видеть, где чередуются сопрограммы. Цикл событий выкинет сопрограмму, когда она ожидает. В этом смысле многопоточность является «настоящей» параллельной моделью. Как я уже сказал, asyncio не является параллельным, пока вы не ожидаете. Я не говорю, что asyncio лучше или хуже.
# Python 3.9.6
import asyncio
import time
async def test(name: str):
print(f"sleeping: {name}")
time.sleep(3) # imagine that this is big chunk of code/ or a number crunching block that takes a while to execute
print(f"awaiting sleep: {name}")
await asyncio.sleep(2)
print(f"woke up: {name}")
async def main():
print("In main")
tasks = [test(name="1"), test(name="2"), test(name="3")]
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
Выход:
In main
sleeping: 1
awaiting sleep: 1
sleeping: 2
awaiting sleep: 2
sleeping: 3
awaiting sleep: 3
woke up: 1
woke up: 2
woke up: 3
Вы можете видеть, что порядок предсказуем, всегда одинаков и синхронен. Никакого чередования. Тогда как при многопоточности вы не можете предсказать порядок (всегда разный).