Какие задачи выполняет event loop в python
Перейти к содержимому

Какие задачи выполняет event loop в python

  • автор:

Полное руководство по модулю asyncio в Python. Часть 3

Сегодня публикуем третью часть (первая, вторая) перевода учебного руководства по модулю asyncio в Python. Здесь представлены разделы оригинала №5, 6 и 7.

5. Определение, создание и запуск корутин

В Python-программах можно определять корутины — так же, как определяют новые подпрограммы (функции).

Функция корутины, после того, как она определена, может быть использована для создания объекта корутины.

Модуль asyncio даёт нам средства для запуска объектов корутин в цикле событий, который представляет собой среду выполнения для корутин.

5.1. Как определить корутину

Корутину можно определить посредством выражения async def .

Это — расширение выражения def , предназначенного для определения подпрограмм.

Оно определяет корутину, которая может быть создана, и возвращает объект корутины.

# определение корутины async def custom_coro(): # . 

Корутину, определённую с помощью async def , называют функцией корутины.

Функция корутины: функция, которая возвращает объект корутины. Функцию корутины можно определить, пользуясь командой async def, она может содержать ключевые слова await, async for и async with.

Python glossary

Затем в пределах корутины могут использоваться выражения, специфичные для корутин, такие, как await , async for и async with .

Выполнение Python-корутины может быть приостановлено и возобновлено во многих местах. Выражения await, async for и async with могут быть использованы только в теле функции корутины.

Coroutine function definition

# определение корутины async def custom_coro(): # ожидание другой корутины await asyncio.sleep(1)

5.2. Как создать корутину

После того, как корутина определена, её можно создать.

Выглядит это как вызов функции.

. создание корутины coro = custom_coro() 

В ходе работы этой команды корутина не запускается.

Эта команда возвращает объект корутины.

Функцию корутины можно рассматривать как фабрику для создания объектов корутины; точнее — не забывайте о том, что вызов функции корутины не приводит к выполнению кода, написанного пользователем. Вместо этого в ходе такого вызова лишь создаётся и возвращается объект корутины.

Python in a Nutshell, 2017, с. 516

У Python-объекта корутины есть методы — такие, как send() и close() . Он имеет тип coroutine .

Продемонстрировать это можно, создав экземпляр корутины и выведя сведения о его типе, воспользовавшись встроенной Python-функцией type() :

# SuperFastPython.com # проверка типа корутины # определение корутины async def custom_coro(): # ожидание другой корутины await asyncio.sleep(1) # создание корутины coro = custom_coro() # проверка типа корутины print(type(coro))

Выполнение кода этого примера приводит к выводу сообщения о том, что созданная корутина относится к классу coroutine .

Нам, кроме того, сообщают об ошибке RuntimeError , так как корутина была создана, но не запускалась. Мы исследуем этот вопрос в следующем разделе.

 sys:1: RuntimeWarning: coroutine 'custom_coro' was never awaited

Объект корутины — это объект, допускающий ожидание.

Это значит, что он представляет Python-тип, реализующий метод await() .

Объекты, допускающие ожидание, обычно реализуют метод await(). Объект корутины, возвращаемый из функции, объявленной с использованием выражения async def — это объект, допускающий ожидание.

Awaitable objects

Подробности об объектах, допускающих ожидание, можно найти здесь.

5.3. Как запустить корутину из Python-кода

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

Цикл событий — это база любого asyncio-приложения. Цикл событий выполняет асинхронные задачи и коллбэки, сетевые операции ввода/вывода, подпроцессы.

Event Loop

Цикл событий, выполняющий корутины, организует работу кооперативной многозадачности, применяемой корутинами.

Код объекта корутины может выполняться лишь тогда, когда работает цикл событий.

Python in a Nutshell, 2017, с. 517

Типичный способ запуска цикла событий для корутин заключается в использовании функции asyncio.run().

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

# SuperFastPython.com # пример запуска корутины import asyncio # определение корутины async def custom_coro(): # ожидание другой корутины await asyncio.sleep(1) # главная корутина async def main(): # выполнение нашей корутины await custom_coro() # запуск программы, основанной на корутинах asyncio.run(main())

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

6. Цикл событий asyncio

Цикл событий — это сердце программ, основанных на asyncio .

В этом разделе мы поговорим о цикле событий.

6.1. Что такое цикл событий asyncio

Цикл событий — это среда для выполнения корутин в одном потоке.

Asyncio — это библиотека для выполнения этих корутин в асинхронной манере с использованием модели конкурентности, известной под названием «однопоточный цикл событий».

Python Concurrency with asyncio, 2022, с. 3

Цикл событий — это важнейший элемент asyncio-программы.

Он отвечает за решение множества задач. Вот некоторые из них:

  • Выполнение корутин.
  • Выполнение коллбэков.
  • Выполнение сетевых операций ввода/вывода.
  • Выполнение подпроцессов.

«Цикл событий» — это распространённый паттерн проектирования, который стал весьма популярным в наши дни благодаря его использованию в JavaScript.

В JavaScript имеется модель среды выполнения, основанная на цикле событий, который отвечает за выполнение кода, за сбор и обработку событий, за выполнение подзадач, поставленных в очередь. Эта модель сильно отличается от моделей из других языков, таких как C и Java.

The event loop, Mozilla

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

Модуль asyncio даёт нам функции для доступа к циклу событий и для организации взаимодействия с ним.

При разработке типичных Python-приложений это не нужно.

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

Разработчикам приложений обычно следует использовать высокоуровневые функции asyncio, такие, как asyncio.run(). У них редко будет возникать необходимость пользоваться ссылкой на объект цикла или вызов его методов.

Event Loop

Модуль asyncio позволяет работать с низкоуровневым API для получения доступа к текущему объекту цикла событий. Этот модуль так же содержит набор методов, которые можно применять для взаимодействия с циклом событий.

Низкоуровневый API предназначен для разработчиков фреймворков, которые могут расширять и дополнять возможности asyncio и интегрировать этот модуль в свои библиотеки.

Обычным разработчикам редко нужно взаимодействовать с циклом событий в программах, основанных на asyncio . Вместо этого они, как правило, применяют высокоуровневый API модуля.

Но, как бы то ни было, мы вполне можем кратко обсудить вопрос использования объекта цикла событий.

6.2. Запуск цикла событий и получение ссылки на его объект

Обычно в asyncio-приложениях ссылки на объекты циклов событий получают, вызывая функцию asyncio.run() .

Эта функция всегда создаёт новый цикл событий и в конце завершает его работу. Её следует использовать как основную точку входа для asyncio-программ, в идеале её нужно вызывать в программах лишь один раз.

Asyncio Coroutines and Tasks

Эта функция принимает корутину и выполняет её до завершения её работы.

Обычно этой функции передают главную корутину, с которой начинается выполнение программы.

Существуют и низкоуровневые функции для создания цикла событий и для работы с ним.

Функция asyncio.new_event_loop() создаёт новый цикл событий и возвращает ссылку на него.

. создаём новый цикл событий asyncio и обеспечиваем доступ к нему loop = asyncio.new_event_loop()

Можно продемонстрировать это всё на рабочем примере.

Здесь мы создаём новый цикл событий и сообщаем сведения о нём.

# SuperFastPython.com # пример создания цикла событий import asyncio # создаём новый цикл событий asyncio и обеспечиваем доступ к нему loop = asyncio.new_event_loop() # сообщаем стандартные сведения о цикле print(loop)

Выполнение этого кода приведёт к созданию цикла событий и к выводу сведений об его объекте.

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

Если цикл событий asyncio уже выполняется — доступ к нему можно получить посредством функции asyncio.get_running_loop().

Возвращает выполняющийся цикл событий в текущем потоке ОС. Если в потоке нет цикла событий — выдаётся ошибка RuntimeError. Эта функция может быть вызвана только из корутины или из коллбэка.

Event Loop

. получаем доступ к выполняющемуся циклу событий loop = asyncio.get_running_loop()

Есть ещё функция, предназначенная для получения или запуска цикла событий. Это — asyncio.get_event_loop(). Но она, в Python 3.10, была признана устаревшей. Пользоваться ей не стоит.

6.3. Подробности об объекте цикла событий

Цикл событий реализован в виде Python-объекта.

Этот объект определяет реализацию цикла событий, он предоставляет стандартный API, предназначенный для взаимодействия с циклом, описанный в классе AbstractEventLoop.

Существуют различные реализации цикла событий для разных платформ.

Например, в ОС семейства Windows и Unix цикл событий будет реализован по-разному из-за различных внутренних механизмов реализации неблокирующего ввода/вывода на этих платформах.

SelectorEventLoop — это цикл событий, используемый по умолчанию в ОС, основанных на Unix — наподобие Linux и macOS.

ProactorEventLoop — это цикл событий, по умолчанию используемый в Windows.

Сторонние библиотеки могут содержать собственные реализации цикла событий ради его оптимизации под специфические задачи.

6.4. Зачем может понадобиться доступ к циклу событий

Зачем нам обращаться к циклу событий за пределами asyncio-программы?

Это может быть нужно по многим причинам.

  1. Для мониторинга хода выполнения задач.
  2. Для выдачи и получения результатов работы задач.
  3. Для запуска одноразовых задач.

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

Цикл событий, кроме того, может быть встроен в обычную asyncio-программу, к нему можно обращаться тогда, когда это нужно.

Теперь, когда мы немного познакомились с циклом событий — перейдём к asyncio-задачам.

7. Создание и запуск asyncio-задач

Объекты Task (задачи) в asyncio-программах можно создавать из корутин.

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

Цикл событий asyncio управляет задачами. Получается, что все корутины в цикле событий становятся задачами, работа с ними тоже ведётся как с задачами.

Поговорим об asyncio-задачах.

7.1. Что такое asyncio-задача

Task — это объект, который отвечает за планирование выполнения asyncio-корутин и за их независимый запуск.

Задача предоставляет средства, предназначенные для планирования выполнения корутин. К этим средствам asyncio-программа может обращаться для получения сведений о корутинах, с их помощью она может взаимодействовать с корутинами.

Задачи создают из корутин. Для создания задачи нужен объект корутины. Задача оборачивает корутину, планирует её выполнение и даёт средства для взаимодействия с ней.

Задачи выполняются независимо друг от друга. Это значит, что их выполнение планируется в цикле событий asyncio , и что они выполняются независимо от того, что ещё происходит в создавшей их корутине. Это отличается от прямого выполнения корутины, когда вызывающая сторона должна дождаться её завершения.

Задачи используются для планирования конкурентного выполнения корутин. Когда корутину оборачивают в объект Task, пользуясь функцией наподобие asyncio.create_task(), выполнение корутины автоматически планируется на ближайшее время.

Coroutines and Tasks

Класс asyncio.Task расширяет класс asyncio.Future, его экземпляры являются объектами, допускающими ожидание.

Future — это низкоуровневый класс. Он представляет собой сущность, которая рано или поздно вернёт результат.

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

Coroutines and Tasks

Классы, которые расширяют класс Future , часто называют Future-подобными классами.

Так как Task — это объект, допускающий ожидание, получается, что корутина может подождать завершения задачи с использованием выражения await .

. подождать завершения задачи await task

Теперь, когда мы разобрались с тем, что собой представляют asyncio-задачи, поговорим о том, как их создавать.

7.2. Как создать задачу

Задачи создают с использованием экземпляра корутины, предоставленного соответствующему механизму.

Вспомните — корутину определяют, используя выражение async def . Она выглядит как функция.

# определение корутины async def task_coroutine(): # . 

Задачу можно создать и запланировать на выполнение только внутри корутины.

Есть два основных способа создания и планирования задач:

  1. Создать объект Task с использованием высокоуровневого API (предпочтительный способ).
  2. Создать объект Task с помощью низкоуровневого API.

Рассмотрим подробнее каждый из этих способов.

Создание объекта Task с использованием высокоуровневого API

Задачу можно создать, прибегнув к функции asyncio.create_task().

Эта функция принимает экземпляр класса корутины и необязательное имя задачи, а возвращает экземпляр класса asyncio.Task .

. создание корутины coro = task_coroutine() создание задачи из корутины task = asyncio.create_task(coro)

Сделать это можно в одной строке, с помощью сложного выражения.

. создание задачи из корутины task = asyncio.create_task(task_coroutine())

Вот что здесь происходит:

  1. Корутина оборачивается в экземпляр Task .
  2. Планируется выполнение задачи в текущем цикле событий.
  3. Возвращается экземпляр Task .

Ссылку на экземпляр Task можно и не сохранять. С задачей можно взаимодействовать посредством методов, её выполнения можно ожидать в корутине.

Это — предпочтительный способ создания объектов Task из корутин в asyncio-программах.

Создание объекта Task с использованием низкоуровневого API

Задачи можно создавать из корутин и с использованием низкоуровневого API asyncio .

Первый способ такого создания задач заключается в использовании функции asyncio.ensure_future().

Эта функция принимает объект Task , Future , или Future-подобный объект, такой, как корутина, и, необязательно — цикл событий, в котором нужно запланировать выполнение соответствующего объекта.

Если цикл событий не предоставлен — выполнение объекта будет запланировано в текущем цикле событий.

Если этой функции предоставлена корутина — она автоматически оборачивается в экземпляр Task , который и возвращает эта функция.

. создание задачи и планирование её выполнения task = asyncio.ensure_future(task_coroutine())

Ещё одна низкоуровневая функция, которую можно использовать для создания объектов Task и для планирования их выполнения, представлена методом loop.create_task().

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

Можно получить ссылку на экземпляр текущего цикла событий, используемого в asyncio-программе, прибегнув к функции asyncio.get_event_loop().

Затем можно вызвать метод create_task() этого экземпляра цикла для создания экземпляра Task и для планирования его выполнения.

. получить текущий цикл событий loop = asyncio.get_event_loop() создать задачу и запланировать её выполнение task = loop.create_task(task_coroutine())

7.3. Когда запускаются задачи?

Распространённый вопрос о работе с задачами звучит так: «Когда, после того, как задача создана, она запускается?».

И это — хороший вопрос.

Хотя мы можем планировать независимый запуск корутин в виде задач, пользуясь функцией create_task() , задача может не запуститься немедленно.

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

Это произойдёт тогда, когда все другие корутины перестанут выполняться и настанет очередь интересующей нас задачи.

Например, имеется asyncio-программа с одной корутиной, которую создали и выполнение которой, виде задачи, запланировали. Запланированная задача не будет выполнена до тех пор, пока вызывающая корутина, создавшая эту задачу, не будет приостановлена.

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

. создание задачи из корутины task = asyncio.create_task(task_coroutine()) ожидание задачи, что позволяет ей запуститься await task

О, а приходите к нам работать? �� ��

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Асинхронный Python: различные формы конкурентности

Асинхронный Python: различные формы конкурентности

Это перевод статьи Абу Ашраф Маснуна «Async Python: The Different Forms of Concurrency».

С появлением Python 3 довольно много шума об «асинхронности» и «параллелизме», можно полагать, что Python недавно представил эти возможности/концепции. Но это не так. Мы много раз использовали эти операции. Кроме того, новички могут подумать, что asyncio является единственным или лучшим способом воссоздать и использовать асинхронные/параллельные операции. В этой статье мы рассмотрим различные способы достижения параллелизма, их преимущества и недостатки.

Определение терминов:

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

Синхронный и асинхронный:

В ​синхронных операциях задачи выполняются друг за другом. В асинхронных — задачи могут запускаться и завершаться независимо друг от друга. Одна асинхронная задача может запускаться и продолжать выполняться, пока выполнение переходит к новой задаче. Асинхронные задачи ​не блокируют (не заставляют ждать завершения выполнения задачи) операции и обычно выполняются в фоновом режиме.

Например, вы должны обратиться в туристическое агентство, чтобы спланировать свой следующий отпуск. Вам нужно отправить письмо своему руководителю, прежде чем улететь. В синхронном режиме, вы сначала позвоните в туристическое агентство, и если вас попросят подождать, то вы будете ждать, пока вам не ответят. Затем вы начнёте писать письмо руководителю. Таким образом, вы выполняете задачи последовательно, одна за другой.

Но, если вы умны, то пока вас попросили подождать, вы начнёте писать письмо, и когда с вами снова заговорят, вы приостановите написание, поговорите, а затем допишете письмо. Вы также можете попросить друга позвонить в агентство, а сами написать письмо. Это асинхронность, задачи не блокируют друг друга.

Конкурентность и параллелизм:

Конкурентность подразумевает, что две задачи выполняются совместно. В нашем предыдущем примере, когда мы рассматривали асинхронный пример, мы постепенно продвигались то в написании письма, то в разговоре с турагентством. Это ​конкурентность.

Когда мы попросили позвонить друга, а сами писали письмо, то задачи выполнялись ​параллельно.​

Параллелизм по сути является формой конкурентности. Но параллелизм зависит от оборудования. Например, если в 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 нужен, чтобы сделать обработку памяти CPython проще и обеспечить наилучшую интеграцию с C.

GIL — это механизм блокировки, когда интерпретатор Python запускает в работу только один поток за раз. Это значит, только один поток может исполняться в байт-коде Python единовременно. GIL следит за тем, чтобы несколько потоков не выполнялись параллельно.

Краткие сведения о GIL:

  • Одновременно может выполняться один поток.
  • Интерпретатор Python переключается между потоками для достижения конкурентности.
  • GIL применим к CPython (стандартной реализации). Но, например, Jython и IronPython не имеют GIL.
  • GIL делает однопоточные программы быстрыми.
  • Операциям ввода/вывода GIL обычно не мешает.
  • GIL позволяет легко интегрировать непотокобезопасные библиотеки на C, благодаря GIL у нас есть много высокопроизводительных расширений/модулей, написанных на C.
  • Для CPU-зависимых задач интерпретатор делает проверку каждые N тиков и переключает потоки. Таким образом один поток не блокирует другие.

Многие видят в GIL слабость. Я же рассматриваю это как благо, ведь были созданы такие библиотеки, как NumPy, SciPy, которые занимают особое, уникальное положение в научном обществе.

Процессы (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

Модуль concurrent.futures большой и позволяет писать асинхронный код очень легко. Мои любимчики — ThreadPoolExecutor и ProcessPoolExecutor. Эти исполнители поддерживают пул потоков или процессов. Мы отправляем наши задачи в пул, и он запускает задачи в доступном потоке / процессе. Возвращается объект 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())

Asyncio — что, как и почему

У вас, вероятно, есть вопрос, который есть у многих людей в сообществе Python — что asyncio приносит нового? Зачем нужен был еще один способ асинхронного ввода-вывода? Разве у нас уже не было потоков и процессов?

Зачем нам нужен asyncio?

Процессы очень дорогостоящие и требуют много ресурсов для создания. Поэтому для операций ввода/вывода в основном выбираются потоки.

Мы знаем, что ввод-вывод зависит от внешних вещей — медленные диски или неприятные сетевые лаги делают ввод-вывод часто непредсказуемым. Теперь предположим, что мы используем потоки для операций ввода-вывода. 3 потока выполняют различные задачи ввода-вывода. Интерпретатор должен был бы переключаться между конкурентными потоками и давать каждому из них некоторое время по очереди.

Назовем потоки — T1, T2 и T3. Три потока начали свою операцию ввода-вывода. T3 завершает его первым. T2 и T1 все еще ожидают ввода-вывода. Интерпретатор Python переключается на T1, но он все еще ждет. Хорошо, интерпретатор перемещается в T2, а тот все еще ждет, а затем перемещается в T3, который готов и выполняет код. Вы видите в этом проблему?

T3 был готов, но интерпретатор сначала переключился между T2 и T1 — это понесло расходы на переключение, которых мы могли бы избежать, если бы интерпретатор сначала переключился на T3, верно?

Что есть asynio?

Asyncio предоставляет нам цикл событий наряду с другими крутыми вещами. Цикл событий (event loop) отслеживает события ввода/вывода и переключает задачи, которые готовы и ждут операции ввода/вывода.

Идея очень проста. Есть цикл обработки событий. И у нас есть функции, которые выполняют асинхронные операции ввода-вывода. Мы передаем свои функции циклу событий и просим его запустить их. Цикл событий возвращает нам объект Future, словно обещание, что в будущем мы что-то получим. Мы держимся за обещание, время от времени проверяем, имеет ли оно значение, и, наконец, когда значение получено, мы используем его в некоторых других операциях.

Как использовать 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")

Такие сложные материи, как асинхронность, мы проходим на обучении Рython ��

Асинхронное программирование на Python

Акчурин И. С.

Отличия между асинхронным и синхронным кодом

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

Асинхронное программирование позволяет запускать операции параллельно, не дожидаясь выполнения последовательности. Это как если бы у вас было восемь рук и вы могли одновременно мыть посуду, пылесосить, читать газету и гладить кота. Жаль, что это невозможно в быту — зато вполне реально в разработке ПО. К тому же асинхронное программирование на Python становится все более популярным.

Где асинхронность применяется в реальном мире

Темы асинхронного программирования часто стараются избегать, особенно на начальных этапах изучения языка. Ее считают сложной и относят к разряду «для посвященных или одаренных» разработчиков. На самом деле «асинхронные дела» окружают нас буквально на каждом шагу.

Основы Python
Курс для IT-специалистов

Например, вы провожаете родственников и просите их позвонить, как только они доберутся до дома. А дальше спокойно занимаетесь своими делами и не названиваете им каждую минуту с проверкой: «Уже доехали?» Потому что звонок от родственников поступит сам, как только они доберутся до дома.

То есть функция «Звонок от родственников» ожидает события «Родственники приехали домой». И как только это событие произойдет, функция сразу сработает.

Или вы ставите напоминание в календаре на конкретную дату и время: «Созвон с коллегами». А потом напрочь забываете об этом событии. Но календарь не забудет!

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

Особенности асинхронного программирования на Python

С чего начать изучение Python, как поймать Python-дзен и для чего этот язык пригодится в повседневной жизни, рассказываем на бесплатном вебинаре «Зачем специалисту Python?».

Обычные функции

Давайте запрограммируем такую ситуацию с уведомлением. Повседневную жизнь представим с помощью функции main ( ) . А уведомление — с помощью функции notification ( ) .

Внутри функции notification ( ) находится задержка в 10 секунд, чтобы имитировать отложенное задание. Если запустить код, то мы увидим, что поговорить с коллегой и поесть не удастся, потому что программа застынет на выполнении notification ( ) и будет ждать 10 секунд, пока уведомление не придет и не спасет от голодной смерти ��

Очевидно, что есть и разговаривать с коллегой мы хотим в то время, пока идет обратный отсчет до уведомления. То есть важно сделать так, чтобы работа функции notification ( ) не прерывала работу функции main ( ) .

Корутины

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

Теперь, если запустим программу, она выполнится мгновенно! Мы и поедим, и с коллегой пообщаемся, вот только помимо этого получим странное предупреждение.

Что же произошло? async — это оператор Python, обертка над основной функцией. Теперь, когда вы вызовете функцию notification ( ) , она вернет специальный объект — корутину (coroutine).

  • Корутина тоже представляет собой функцию, которую нужно вызвать отдельно. Об этом и сообщает Python.

Обычный вызов notification ( ) теперь не исполняет функцию, а превращает ее в корутину, которую нужно вызвать. Чтобы это сделать, используем оператор await .

Теперь запуск кода приведет к новой проблеме:

Python будет выполнять await только в том случае, если он будет вызван внутри асинхронной функции. То есть функцию main ( ) необходимо тоже сделать асинхронной.

Но тогда мы получили еще одну корутину, которую также кто-то должен вызвать и сделать это опять только внутри асинхронной функции! Что же делать? Замкнутый круг ��

Asyncio спешит на помощь

Для управления асинхронными функциями в Python используется специальный модуль asyncio. Его поддержка впервые появилась в версии Python 3.5 в 2015 году. Добавим его к программе и запустим корутину main ( ) , используя метод run( ).

В терминах асинхронного программирования функция main ( ) будет называться циклом обработки событий (event loop). Это основной цикл программы, который вызывает все асинхронные функции, необходимые для работы.

Теперь программа вновь заработает! Правда, уведомление о созвоне снова не даст поесть и пообщаться с коллегой… Все опять работает как обычная последовательность действий.

Дело в том, что недостаточно просто запустить корутину — необходимо создать задачу (task), которая это сделает.

Ура! Наконец можно спокойно пообедать, поговорить с коллегой и заняться любыми другими делами. А уведомление о встрече придет тогда, когда истечет таймер, — в нашем случае это 10 секунд.

Запустите программу и обратите внимание на последовательность работы функции print( ). Несмотря на то что «Едим» и «Разговариваем с коллегой» выполняются после вызова корутины notification ( ) , мы увидим их первыми, потому что корутина выполняется асинхронно относительно основного цикла обработки событий main ( ) .

Знатоки Python могут закидать меня помидорами за такой пример, так как на самом деле time.sleep( ) нельзя использовать при создании нескольких асинхронных событий внутри event loop. А также не хватает вызова корутины через await . Но не торопитесь: обо всем по порядку.

Когда работы много

Зачем специалисту Python?
Бесплатный вебинар

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

Создадим основную функцию, внутри которой будем вызывать функции, возвращающие данные для погодной станции. Чтобы имитировать задержки, воспользуемся функцией time.sleep( ). Также будем измерять общее время
работы программы с помощью time.time( ).

Традиционное решение — вызов функций друг за другом. Так как выполнение main ( ) приостанавливается в момент вызова get_temp ( ) и get_pres ( ) , несложно подсчитать, что общее время работы будет равно сумме времени, затраченного на выполнение всех функций, вызываемых внутри main ( ) .

Еще бы! Ведь сейчас эти функции выполняются последовательно друг за другом — измерение давления не начнется, пока не закончится измерение температуры. Но зачем ждать, если одно вообще не зависит от другого? Перепишем код, сделав функции запроса асинхронными.

Теперь, если запустим программу, увидим странную особенность: функции измерения работают последовательно — асинхронностью здесь и не пахнет. Программа выполняется все так же долго — 6 секунд. А терминал вообще вывел информацию в странном порядке.

Почему так происходит? Все дело в задержках, которые мы создали. Асинхронно сейчас выполняется только main ( ) (цикл событий) относительно функций, которые он вызывает. Это видно по моментальному выводу о начале и конце измерения еще до того, как мы получили непосредственно сами данные.

А вот функции get_temp ( ) и get_pres ( ) выполняются последовательно, потому что внутри содержат обычную функцию time.sleep( ), тормозящую всю работу. Чтобы исправить этот эффект, необходимы корутины, которые будут создавать задержку и при этом не будут тормозить вызов других функций. Исправьте time.sleep( ) на asyncio.sleep( ) и не забудьте про await , ведь теперь мы работаем с корутинами.

Если запустим программу, то убедимся в том, что функции действительно стали работать асинхронно. Вывод происходит моментально, идет запрос информации и… на этом программа завершается, показывая время работы 0 секунд.

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

И тут на помощь снова придет await . Этот оператор не только исполняет корутину, но еще и дает указание циклу событий дождаться завершения выполнения этих корутин прежде, чем завершиться самому. Вот в таком виде программа будет работать корректно — именно так, как планировалось:

И время выполнения программы сократилось до 4 секунд вместо 6:

Теперь в цикл событий main ( ) можно добавить сколько угодно задач через asyncio. А общее время его работы всегда будет определяться временем работы самой долгой функции. Добро пожаловать в асинхронный мир!

Асинхронное программирование. Библиотека asyncio.

В программировании эти два понятия достаточно близки друг к другу, однако в то же время имеют существенную разницу.

  • Parallel execution (параллелизм) — исполнение нескольких задач одновременно. Для достижения параллелизма необходимо физическое одноврменное исполнение задач (multithreading и multiprocessing).
  • Concurrency (конкурентность) — две или более задачи могут запускаться, выполняться и завершаться в перекрывающиеся периоды времени.

Основным отличием конкурентности является то, что это логическое распараллеливание программы. Если в вашем ПК только одно ядро, то в один момент времени может исполняться только одна задача. Однако в случае конкурентности исполенение задач может приостанавливаться с целью переключения на другие задачи. Тем самым прогресс исполнения нескольких задач может идти одновременно по нескольким задачам даже в случае физически невозможного их одновременного исполнения.

Асинхронные операции и обратные вызовы

Как упоминалось в предыдущей теме, мы можем хотим продолжать вычисления параллельно с операциями ввода/вывода. Такие операции достаточно медленные, и их вызов блокирует поток исполнения программы. Поэтому эти операции выносили в отдельный поток.

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

# функция, отвечающая за обработку ответа def handle_response(response): print('\n . '.format(response.body)) # создание объекта для общения с сетью http_client = AsyncHTTPClient() # неблокирующий вызов функции! # после вызова функции fetch будет выполняться следующий за этой строчкой код без ожидания получения ответа # ответ с сайта будет обработан функцией handle_response (так называемым callback'ом) http_client.fetch('http://yandex.ru', callback=handle_response) 

Проблема данного подхода заключается в том, что внутри одной callback функции может быть вызвана другая и т.д. Такой код становится трудно читаем, а стек вызова становится достаточно запутанным.

Корутины и asyncio

Корутина (coroutine) — подпрограмма (функция), которая может начинаться, приостанавливаться и завершаться в произвольный момент времени. Корутины описываются синтаксисом async/await.

Сходу рассмотрим несколько примеров.

import asyncio async def main(): print('hello') await asyncio.sleep(1) print('world') loop = asyncio.get_event_loop() # Blocking call which returns when the main() coroutine is done loop.run_until_complete(main()) loop.close() 
  1. Упарвление будет передано в корутину main;
  2. Напечатается hello;
  3. Управление будет передано в корутину sleep;
  4. В течении одной секунды программа «спит»;
  5. Управление вернется в main;
  6. Напечатается wolrd;
  7. Корутина main, а далее сама программа завершаются.

Следующий пример напечатает “hello” после ожидания в 1 секунду, а затем напечатает “world” после ожидания в 2 секунды:

import asyncio import time async def say_after(delay, what): await asyncio.sleep(delay) print(what) async def main(): print(f"started at time.strftime('%X')>") await say_after(1, 'hello') await say_after(2, 'world') print(f"finished at time.strftime('%X')>") loop = asyncio.get_event_loop() # Blocking call which returns when the main() coroutine is done loop.run_until_complete(main()) loop.close() 

В следующем примере мы запустим две задачи на параллельное исполнение.

import asyncio import time async def say_after(delay, what): await asyncio.sleep(delay) print(what) loop = asyncio.get_event_loop() # Ожидание завершения обоих операций должно занять около 2х секунд print(f"started at time.strftime('%X')>") loop.run_until_complete(asyncio.gather( say_after(1, 'hello'), say_after(2, 'world') )) print(f"finished at time.strftime('%X')>") loop.close() 

asyncio абстракции

При работе с асинхронностью мы встретились с понятием цикл событий (event loop). Это программная конструкция, которая управляет выполнением различных задач: регистрирует поступление и запускает в подходящий момент.

C помощью синтаксиса await мы определяем места, где можно переключиться на другие ожидающие выполнения задачи.

Рассмотрим подробнее следующий пример:

import asyncio async def compute(a, b): print('Compute. ') await asyncio.sleep(1.0) return a + b async def print_sum(a, b): result = await compute(a, b) print('<> + <> = <>'.format(a, b, result)) loop = asyncio.get_event_loop() loop.run_until_complete(print_sum(1, 2)) loop.close() 

Начиная с версии Python 3.7 синтаксис работы с библиотекой (а именно создание цикла событий) был упрощен. Подробнее про библиотеку можно узнать здесь.

Упражнение №1

Что будет напечатано и почему?

import asyncio async def factorial(name, number): f = 1 for i in range(2, number + 1): print(f"Task name>: Compute factorial(i>). ") await asyncio.sleep(1) f *= i print(f"Task name>: factorial(number>) = f>") async def main(): await asyncio.gather( factorial("A", 2), factorial("B", 3), factorial("C", 4), ) loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close() 

Waiting & timeouts

Иногда выполнение операции может занять очень длительное время. Например, вы до сих пор не получили ответ от сервера. В случае отсутствия соединения ваша операция может висеть бесконечно долго. В таком случае на асинхронные операции имеет смысл выставлять timeout. Пример на выставление timeout:

import asyncio async def eternity(): # Sleep for one hour await asyncio.sleep(3600) print('yay!') async def main(): # Wait for at most 1 second try: await asyncio.wait_for(eternity(), timeout=1.0) except asyncio.TimeoutError: print('timeout!') loop = asyncio.get_event_loop() loop.run_until_complete(main()) 

async with

Асинхронный контекстный менеджер — это контекстный менджер, который умеет приостанавливать выполнение в методах входа и выхода: __aenter__(), __aexit__()

lock = asyncio.Lock() # . later await lock.acquire() try: # access shared state finally: lock.release() 
lock = asyncio.Lock() # . later async with lock: # access shared state 

Futures

Футуры (futures) — объекты, в которых хранится текущий результат выполнения какой-либо задачи. Это может быть информация о том, что задача ещё не обработана или уже полученный результат; а может быть вообще исключение.

Одна из особенностей футур, что мы можем запустить задачу на исполнение в одной корутине, а получить результат выполнения в другой. У футур есть 4 возможных состояния: + ожидание (pending) + выполнение (running) + выполнено (done) + отменено (cancelled)

Когда футура находится в состояние done, у неё можно получить результат выполнения. В состояниях pending и running такая операция приведёт к исключению InvalidStateError, а в случае canelled будет CancelledError, и наконец, если исключение произошло в самой корутине, оно будет сгенерировано снова при попытке получить результат.

Узнать состояние футуры можно с помощью методов done() или cancelled(), Вызов result() возвращает ожидаемый результат. Для получения исключения есть метод exception(). Для отмены выполнения футуры есть метод cancel(). И result() и exception() выбросят CancelledError, если футура была остановлена в процессе работы.

Ожидание окончания футуры можно сделать при помощи функции wait_for(). Первый аргумент — футура, второй — таймаут (None, если таймаут не нужен).

import asyncio async def set_after(delay, value): # Sleep for *delay* seconds. await asyncio.sleep(delay) # Set *value* as a result of *fut* Future. return value async def main(): # Run "set_after()" coroutine in a parallel Task. fut = asyncio.ensure_future( set_after(1, '. world')) print('hello . ') # Wait until *fut* has a result (1 second) await asyncio.wait_for(fut, None) # and print it. print(fut.result()) loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close() 

Возможен запуск футуры при помощи await.

async def main(): # Run "set_after()" coroutine in a parallel Task. fut = asyncio.ensure_future( set_after(1, '. world')) print('hello . ') # Wait until *fut* has a result (1 second) and print it. # Alternative way to get a result, just use it. print(await fut) 

Упраженения на дом

aiohttp

Рядом с asyncio создано огромное количество асинхронных модулей для решения всевозможных задач. aiohttp — лишь одна из них. Это асинхронный HTTP Клиент/Сервер

В следующем примере получаем содержимое страницы google.com: (при отсутствии доступа в интернет, cs.mipt.ru)

import aiohttp async with aiohttp.ClientSession() as session: async with session.get('http://google.com') as resp: text = await resp.text() print(' . '.format(text)) 

Реализация простого сервера:

from aiohttp import web async def handle(request): name = request.match_info.get('name', 'Anonymous') text = 'Hello, ' + name # . # здесь идет некоторая дополнительная логика с async/await # return web.Response(text=text) app = web.Application() app.add_routes([web.get('/', handle), web.get('/ ', handle)]) web.run_app(app) 
Упражнение №2

Узнать свой IP адрес. Есть куча сервисов, которые позволяют узнать ваш ip. Но на момент запуска программы вы не знаете какой из сервисов доступен. Вместо того, чтобы опрашивать каждый из этих сервисов последовательно, можно запустить все запросы конкурентно и выбрать первый успешный.

При отсутствии доступа в интернет симулируйте задачу через cs.mipt.ru (к примеру, получение страниц вида cs.mipt.ru/advanced_python/lessons/labX.html и выбора первой, в которой количество символов больше, чем N)

Потребуется asyncio.wait() и параметр return_when

from collections import namedtuple import time import asyncio from concurrent.futures import FIRST_COMPLETED import aiohttp Service = namedtuple('Service', ('name', 'url', 'ip_attr')) SERVICES = ( Service('ipify', 'https://api.ipify.org?format=json', 'ip'), Service('ip-api', 'http://ip-api.com/json', 'query') ) async def fetch_ip(service): # получение ip async def asynchronous(): # TODO: # создание футур для сервисов # используйте FIRST_COMPLETED ioloop = asyncio.get_event_loop() ioloop.run_until_complete(asynchronous()) 

aiogram

Это библиотека для написания асинхронного Telegram бота.

Упражнение №3

Напишите телеграм бота, который будет на сообщение присылать соответствующее изображение

  • установить aiogram 1.4 — асинхронная обертка над api телеграмма
  • поговорить с @FatherBot, создать бота и запомнить выданный токен
  • В рф нужно использовать впн или прокси (в сети есть огромное количество списков адресов)
  • разобраться с примером эхо бота ниже
  • написать требуемый функционал (картинки можно запрашивать через поиск яндекса или гугла, существуют готовые api, можно написать и самостоятельно)
from aiogram import Bot, types from aiogram.dispatcher import Dispatcher from aiogram.utils import executor PROXY_URL = 'socks5://xxx.xxx.xxx.xxx' # вставить здесь подходящий ip secret_token = 'XXX' # строка вида: 123456789:ABCDEFGHJABCDEFGHJABCDEFGHJABCDEFGHJ bot = Bot(token=secret_token, proxy=PROXY_URL) dp = Dispatcher(bot) @dp.message_handler(commands=['start', 'help']) async def send_welcome(message: types.Message): await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.") @dp.message_handler() async def echo(message: types.Message): await message.reply(message.text) if __name__ == '__main__': executor.start_polling(dp) 

Сайт построен с использованием Pelican. За основу оформления взята тема от Smashing Magazine. Исходные тексты программ, приведённые на этом сайте, распространяются под лицензией GPLv3, все остальные материалы сайта распространяются под лицензией CC-BY.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *