[Перевод] Полное руководство по модулю 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 () создаёт новый цикл событий и возвращает ссылку на него.
Создаёт и возвращает новый объект цикла событий.
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-программы?
Это может быть нужно по многим причинам.
Например:
Для мониторинга хода выполнения задач.
Для выдачи и получения результатов работы задач.
Для запуска одноразовых задач.
Цикл событий 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():
# ...
Задачу можно создать и запланировать на выполнение только внутри корутины.
Есть два основных способа создания и планирования задач:
Создать объект
Task
с использованием высокоуровневого API (предпочтительный способ).Создать объект
Task
с помощью низкоуровневого API.
Рассмотрим подробнее каждый из этих способов.
Создание объекта Task с использованием высокоуровневого API
Задачу можно создать, прибегнув к функции asyncio.create_task ().
Эта функция принимает экземпляр класса корутины и необязательное имя задачи, а возвращает экземпляр класса asyncio.Task
.
Например:
...
создание корутины
coro = task_coroutine()
создание задачи из корутины
task = asyncio.create_task(coro)
Сделать это можно в одной строке, с помощью сложного выражения.
Например:
...
создание задачи из корутины
task = asyncio.create_task(task_coroutine())
Вот что здесь происходит:
Корутина оборачивается в экземпляр
Task
.Планируется выполнение задачи в текущем цикле событий.
Возвращается экземпляр
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
На сегодня всё :)
О, а приходите к нам работать?