[Перевод — recovery mode ] Асинхронный Python: различные формы конкурентности
С появлением Python 3 довольно много шума об “асинхронности” и “параллелизме”, можно полагать, что Python недавно представил эти возможности/концепции. Но это не так. Мы много раз использовали эти операции. Кроме того, новички могут подумать, что asyncio является единственным или лучшим способом воссоздать и использовать асинхронные/параллельные операции. В этой статье мы рассмотрим различные способы достижения параллелизма, их преимущества и недостатки.
Определение терминов:
Прежде чем мы углубимся в технические аспекты, важно иметь некоторое базовое понимание терминов, часто используемых в этом контексте.
Синхронный и асинхронный:
В асинхронных задачи могут запускаться и завершаться независимо друг от друга. В синхронных операциях задачи выполняются друг за другом. Асинхронные задачи не блокируют (не заставляют ждать завершения выполнения задачи) операции и обычно выполняются в фоновом режиме. Одна асинхронная задача может запускаться и продолжать выполняться, пока выполнение переходит к новой задаче.
Вам нужно отправить письмо своему руководителю, прежде чем улететь. Например, вы должны обратиться в туристическое агентство, чтобы спланировать свой следующий отпуск. Затем вы начнёте писать письмо руководителю. В синхронном режиме, вы сначала позвоните в туристическое агентство, и если вас попросят подождать, то вы будете ждать, пока вам не ответят. [синхронное выполнение, прим. Таким образом, вы выполняете задачи последовательно, одна за одной. переводчика] вы начнёте писать e-mail и когда с вами снова заговорят вы приостановите написание, поговорите, а затем допишете письмо. переводчика] Но, если вы умны, то пока вас попросили подождать [повисеть на телефоне, прим. Это асинхронность, задачи не блокируют друг друга. Вы также можете попросить друга позвонить в агентство, а сами написать письмо.
Конкурентность и параллелизм:
В нашем предыдущем примере, когда мы рассматривали асинхронный пример, мы постепенно продвигались то в написании письма, то в разговоре с тур. Конкурентность подразумевает, что две задачи выполняются совместно. Это конкурентность. агентством.
Когда мы попросили позвонить друга, а сами писали письмо, то задачи выполнялись параллельно.
Но параллелизм зависит от оборудования. Параллелизм по сути является формой конкурентности. Они просто делят процессорное время между собой. Например, если в CPU только одно ядро, то две задачи не могут выполняться параллельно. Но когда у нас есть несколько ядер [как друг в предыдущем примере, который является вторым ядром, прим. Тогда это конкурентность, но не параллелизм. переводчика] мы можем выполнять несколько операций (в зависимости от количества ядер) одновременно.
Подытожим:
- Синхронность: блокирует операции (блокирующие)
- Асинхронность: не блокирует операции (неблокирующие)
- Конкурентность: совместный прогресс (совместные)
- Параллелизм: параллельный прогресс (параллельные)
Параллелизм подразумевает конкурентность. Но конкурентность не всегда подразумевает параллелизм.
Потоки и процессы
Python поддерживает потоки уже очень давно. Потоки позволяют выполнять операции конкурентно. Но есть проблема, связанная с Global Interpreter Lock (GIL) из-за которой потоки не могли обеспечить настоящий параллелизм. И тем не менее, с появлением multiprocessing можно использовать несколько ядер с помощью Python.
Потоки (Threads)
В нижеследующем коде функция worker будет выполняться в нескольких потоках асинхронно и одновременно. Рассмотрим небольшой пример.
import threading
import time
import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker , I slept for {} seconds".format(number, sleep)) for i in range(5): t = threading.Thread(target=worker, args=(i,)) t.start() print("All Threads are queued, let's see when they finish!")
А вот пример выходных данных:
$ python thread_test.py
All Threads are queued, let's see when they finish!
I am Worker 1, I slept for 1 seconds
I am Worker 3, I slept for 4 seconds
I am Worker 4, I slept for 5 seconds
I am Worker 2, I slept for 7 seconds
I am Worker 0, I slept for 9 seconds
Таким образом мы запустили 5 потоков для совместной работы и после их старта (т.е. после запуска функции worker) операция не ждёт завершения работы потоков прежде чем перейти к следующему оператору print. Это асинхронная операция.
Если бы мы хотели, то могли бы реализовать подкласс с методом (ООП стиль). В нашем примере мы передали функцию в конструктор Thread.
Дальнейшее чтение:
Чтобы узнать больше о потоках, воспользуйтесь ссылкой ниже:
Global Interpreter Lock (GIL)
GIL — это механизм блокировки, когда интерпретатор Python запускает в работу только один поток за раз. GIL был представлен, чтобы сделать обработку памяти CPython проще и обеспечить наилучшую интеграцию с C(например, с расширениями). только один поток может исполняться в байт-коде Python единовременно. Т.е. GIL следит за тем, чтобы несколько потоков не выполнялись параллельно.
Краткие сведения о GIL:
- Одновременно может выполняться один поток.
- Интерпретатор Python переключается между потоками для достижения конкурентности.
- GIL применим к CPython (стандартной реализации). Но такие как, например, Jython и IronPython не имеют GIL.
- GIL делает однопоточные программы быстрыми.
- Операциям ввода/вывода GIL обычно не мешает.
- GIL позволяет легко интегрировать непотокобезопасные библиотеки на C, благодаря GIL у нас есть много высокопроизводительных расширений/модулей, написанных на C.
- Для CPU зависимых задач интерпретатор делает проверку каждые N тиков и переключает потоки. Таким образом один поток не блокирует другие.
Многие видят в GIL слабость. Я же рассматриваю это как благо, ведь были созданы такие библиотеки как NumPy, SciPy, которые занимают особое, уникальное положение в научном обществе.
Дальнейшее чтение:
Эти ресурсы позволят углубиться в GIL:
Процессы (Processes)
Чтобы достичь параллелизма в Python был добавлен модуль multiprocessing, который предоставляет API, и выглядит очень похожим, если вы использовали threading раньше.
Теперь модифицированная версия использует Процесс вместо Потока. Давайте просто пойдем и изменим предыдущий пример.
import multiprocessing
import time
import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker {}, I slept for {} seconds".format(number, sleep)) for i in range(5): t = multiprocessing.Process(target=worker, args=(i,)) t.start() print("All Processes are queued, let's see when they finish!")
Что же изменилось? Я просто импортировал модуль multiprocessing вместо threading. А затем, вместо потока я использовал процесс. Вот и всё! Теперь вместо множества потоков мы используем процессы которые запускаются на разных ядрах CPU (если, конечно, у вашего процессора несколько ядер).
Пример из официальных документов: С помощью класса Pool мы также можем распределить выполнение одной функции между несколькими процессами для разных входных значений.
from multiprocessing import Pool def f(x): return x*x if __name__ == '__main__': p = Pool(5) print(p.map(f, [1, 2, 3]))
Здесь вместо того, чтобы перебирать список значений и вызывать функцию f по одному, мы фактически запускаем функцию в разных процессах. Один процесс выполняет f(1), другой-f(2), а другой-f (3). Наконец, результаты снова объединяются в список. Это позволяет нам разбить тяжелые вычисления на более мелкие части и запускать их параллельно для более быстрого расчета.
Дальнейшее чтение:
Модуль concurrent.futures
Мои любимчики ThreadPoolExecutor и ProcessPoolExecutor. Модуль concurrent.futures большой и позволяет писать асинхронный код очень легко. Мы отправляем наши задачи в пул, и он запускает задачи в доступном потоке / процессе. Эти исполнители поддерживают пул потоков или процессов. Возвращается объект Future, который можно использовать для запроса и получения результата по завершении задачи.
А вот пример ThreadPoolExecutor:
from concurrent.futures import ThreadPoolExecutor
from time import sleep def return_after_5_secs(message): sleep(5) return message pool = ThreadPoolExecutor(3) future = pool.submit(return_after_5_secs, ("hello"))
print(future.done())
sleep(5)
print(future.done())
print(future.result())
У меня есть статья о concurrent.futures masnun.com/2016/03/29/python-a-quick-introduction-to-the-concurrent-futures-module.html. Она может быть полезна при более глубоком изучении этого модуля.
Дальнейшее чтение:
Asyncio — что, как и почему?
У вас, вероятно, есть вопрос, который есть у многих людей в сообществе Python — что asyncio приносит нового? Зачем нужен был еще один способ асинхронного ввода-вывода? Разве у нас уже не было потоков и процессов? Давай посмотрим!
Зачем нам нужен asyncio?
переводчика] для создания. Процессы очень дорогостоящие [с точки зрения потребления ресурсов, прим. Мы знаем, что ввод-вывод зависит от внешних вещей — медленные диски или неприятные сетевые лаги делают ввод-вывод часто непредсказуемым. Поэтому для операций ввода/вывода в основном выбираются потоки. 3 потока выполняют различные задачи ввода-вывода. Теперь предположим, что мы используем потоки для операций ввода-вывода. Назовем потоки — T1, T2 и T3. Интерпретатор должен был бы переключаться между конкурентными потоками и давать каждому из них некоторое время по очереди. T3 завершает его первым. Три потока начали свою операцию ввода-вывода. Интерпретатор Python переключается на T1, но он все еще ждет. T2 и T1 все еще ожидают ввода-вывода. Вы видите в этом проблему? Хорошо, интерпретатор перемещается в T2, а тот все еще ждет, а затем перемещается в T3, который готов и выполняет код.
T3 был готов, но интерпретатор сначала переключился между T2 и T1 — это понесло расходы на переключение, которых мы могли бы избежать, если бы интерпретатор сначала переключился на T3, верно?
Что есть asynio?
Цикл событий (event loop) отслеживает события ввода/вывода и переключает задачи, которые готовы и ждут операции ввода/вывода [цикл событий — программная конструкция, которая ожидает прибытия и производит рассылку событий или сообщений в программе, прим. Asyncio предоставляет нам цикл событий наряду с другими крутыми вещами. переводчика].
Есть цикл обработки событий. Идея очень проста. Мы передаем свои функции циклу событий и просим его запустить их для нас. И у нас есть функции, которые выполняют асинхронные операции ввода-вывода. Мы держимся за обещание, время от времени проверяем, имеет ли оно значение (нам очень не терпится), и, наконец, когда значение получено, мы используем его в некоторых других операциях [т.е. Цикл событий возвращает нам объект Future, словно обещание, что в будущем мы что-то получим. Мы периодически проверяем результат и как только он получен мы берем билет и по нему получаем значение, прим. мы послали запрос, нам сразу дали билет и сказали ждать, пока придёт результат. переводчика].
Прочитать детали вы можете здесь: Asyncio использует генераторы и корутины для остановки и возобновления задач.
Как использовать asyncio?
Прежде чем мы начнём, давайте взглянем на пример:
import asyncio
import datetime
import random async def my_sleep_func(): await asyncio.sleep(random.randint(0, 5)) async def display_date(num, loop): end_time = loop.time() + 50.0 while True: print("Loop: {} Time: {}".format(num, datetime.datetime.now())) if (loop.time() + 1.0) >= end_time: break await my_sleep_func() loop = asyncio.get_event_loop() asyncio.ensure_future(display_date(1, loop))
asyncio.ensure_future(display_date(2, loop)) loop.run_forever()
Обратите внимание, что синтаксис async/await предназначен только для Python 3.5 и выше. Пройдёмся по коду:
- У нас есть асинхронная функция display_date, которая принимает число (в качестве идентификатора) и цикл обработки событий в качестве параметров.
- Функция имеет бесконечный цикл, который прерывается через 50 секунд. Но за этот период, она неоднократно печатает время и делает паузу. Функция await может ожидать завершения выполнения других асинхронных функций (корутин).
- Передаем функцию в цикл обработки событий (используя метод ensure_future).
- Запускаем цикл событий.
Всякий раз, когда происходит вызов await, asyncio понимает, что функции, вероятно, потребуется некоторое время. Таким образом, он приостанавливает выполнение, начинает мониторинг любого связанного с ним события ввода-вывода и позволяет запускать задачи. Когда asyncio замечает, что приостановленный ввод-вывод функции готов, он возобновляет функцию.
Делаем правильный выбор
Только что мы прошлись по самым популярным формам конкурентности. Но остаётся вопрос — что следует выбрать? Это зависит от вариантов использования. Из моего опыта я склонен следовать этому псевдо-коду:
if io_bound: if io_very_slow: print("Use Asyncio") else: print("Use Threads")
else: print("Multi Processing")
- CPU Bound => Multi Processing
- I/O Bound, Fast I/O, Limited Number of Connections => Multi Threading
- I/O Bound, Slow I/O, Many connections => Asyncio
[Прим. переводчика]