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

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

922fc073942729bb902c907c8f09e609.png

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 () создаёт новый цикл событий и возвращает ссылку на него.

Создаёт и возвращает новый объект цикла событий.

Event Loop

Например:

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

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

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

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

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

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

<_UnixSelectorEventLoop running=False closed=False debug=False>

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

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

PEP 3156 — Asynchronous IO Support Rebooted: the «asyncio» Module

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

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

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

Coroutines and Tasks

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

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

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

Coroutines and Tasks

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

Future-подобный объект, который отвечает за выполнение Python-корутин.

Coroutines and Tasks

Так как 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

На сегодня всё :)

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

© Habrahabr.ru