Сегодня публикуем третью часть (первая, вторая) перевода учебного руководства по модулю asyncio в Python. Здесь представлены разделы оригинала №5, 6 и 7. В Python-программах можно определять корутины — так же, как определяют новые подпрограммы (функции). Функция корутины, после того, как она определена, может быть использована для создания объекта корутины. Модуль asyncio даёт нам средства для запуска объектов корутин в цикле событий, который представляет собой среду выполнения для корутин. Корутину можно определить посредством выражения async def. Это — расширение выражения def, предназначенного для определения подпрограмм. Оно определяет корутину, которая может быть создана, и возвращает объект корутины. Например: Корутину, определённую с помощью 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 Например: После того, как корутина определена, её можно создать. Выглядит это как вызов функции. Например: В ходе работы этой команды корутина не запускается. Эта команда возвращает объект корутины. Функцию корутины можно рассматривать как фабрику для создания объектов корутины; точнее — не забывайте о том, что вызов функции корутины не приводит к выполнению кода, написанного пользователем. Вместо этого в ходе такого вызова лишь создаётся и возвращается объект корутины. Python in a Nutshell, 2017, с. 516 У Python-объекта корутины есть методы — такие, как send() и close(). Он имеет тип coroutine. Продемонстрировать это можно, создав экземпляр корутины и выведя сведения о его типе, воспользовавшись встроенной Python-функцией type(): Выполнение кода этого примера приводит к выводу сообщения о том, что созданная корутина относится к классу coroutine. Нам, кроме того, сообщают об ошибке RuntimeError, так как корутина была создана, но не запускалась. Мы исследуем этот вопрос в следующем разделе. Объект корутины — это объект, допускающий ожидание. Это значит, что он представляет Python-тип, реализующий метод await(). Объекты, допускающие ожидание, обычно реализуют метод await(). Объект корутины, возвращаемый из функции, объявленной с использованием выражения async def — это объект, допускающий ожидание. Awaitable objects Подробности об объектах, допускающих ожидание, можно найти здесь. Корутины можно определять и создавать в обычном Python-коде, но запускать их можно только в цикле событий. Цикл событий — это база любого asyncio-приложения. Цикл событий выполняет асинхронные задачи и коллбэки, сетевые операции ввода/вывода, подпроцессы. Event Loop Цикл событий, выполняющий корутины, организует работу кооперативной многозадачности, применяемой корутинами. Код объекта корутины может выполняться лишь тогда, когда работает цикл событий. Python in a Nutshell, 2017, с. 517 Типичный способ запуска цикла событий для корутин заключается в использовании функции asyncio.run(). Эта функция принимает одну корутину и возвращает значение корутины. Предоставленная ей корутина может быть использована как точка входа в программу, основанную на корутинах. Например: Теперь, когда мы знаем о том, как определять, создавать и запускать корутины — поближе познакомимся с циклом событий. Цикл событий — это сердце программ, основанных на 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 модуля. Но, как бы то ни было, мы вполне можем кратко обсудить вопрос использования объекта цикла событий. Обычно в asyncio-приложениях ссылки на объекты циклов событий получают, вызывая функцию asyncio.run(). Эта функция всегда создаёт новый цикл событий и в конце завершает его работу. Её следует использовать как основную точку входа для asyncio-программ, в идеале её нужно вызывать в программах лишь один раз. Asyncio Coroutines and Tasks Эта функция принимает корутину и выполняет её до завершения её работы. Обычно этой функции передают главную корутину, с которой начинается выполнение программы. Существуют и низкоуровневые функции для создания цикла событий и для работы с ним. Функция asyncio.new_event_loop() создаёт новый цикл событий и возвращает ссылку на него. Создаёт и возвращает новый объект цикла событий. Event Loop Например: Можно продемонстрировать это всё на рабочем примере. Здесь мы создаём новый цикл событий и сообщаем сведения о нём. Выполнение этого кода приведёт к созданию цикла событий и к выводу сведений об его объекте. В данном случае можно видеть, что цикл событий имеет тип _UnixSelectorEventLoop, и что он не выполняется, но и не является закрытым. Если цикл событий asyncio уже выполняется — доступ к нему можно получить посредством функции asyncio.get_running_loop(). Возвращает выполняющийся цикл событий в текущем потоке ОС. Если в потоке нет цикла событий — выдаётся ошибка RuntimeError. Эта функция может быть вызвана только из корутины или из коллбэка. Event Loop Например: Есть ещё функция, предназначенная для получения или запуска цикла событий. Это — asyncio.get_event_loop(). Но она, в Python 3.10, была признана устаревшей. Пользоваться ей не стоит. Цикл событий реализован в виде Python-объекта. Этот объект определяет реализацию цикла событий, он предоставляет стандартный API, предназначенный для взаимодействия с циклом, описанный в классе AbstractEventLoop. Существуют различные реализации цикла событий для разных платформ. Например, в ОС семейства Windows и Unix цикл событий будет реализован по-разному из-за различных внутренних механизмов реализации неблокирующего ввода/вывода на этих платформах. SelectorEventLoop — это цикл событий, используемый по умолчанию в ОС, основанных на Unix — наподобие Linux и macOS. ProactorEventLoop — это цикл событий, по умолчанию используемый в Windows. Сторонние библиотеки могут содержать собственные реализации цикла событий ради его оптимизации под специфические задачи. Зачем нам обращаться к циклу событий за пределами asyncio-программы? Это может быть нужно по многим причинам. Например: Для мониторинга хода выполнения задач. Для выдачи и получения результатов работы задач. Для запуска одноразовых задач. Цикл событий asyncio может использоваться в программах как альтернатива пулу потоков, рассчитанная на работу с задачами, основанными на корутинах. Цикл событий, кроме того, может быть встроен в обычную asyncio-программу, к нему можно обращаться тогда, когда это нужно. Теперь, когда мы немного познакомились с циклом событий — перейдём к asyncio-задачам. Объекты Task (задачи) в asyncio-программах можно создавать из корутин. Задачи предоставляют инструменты, предназначенные для независимого планирования и выполнения корутин. Они позволяют ставить задачи в очередь и отменять их, получать в нужное время результаты их работы и выданные ими исключения. Цикл событий asyncio управляет задачами. Получается, что все корутины в цикле событий становятся задачами, работа с ними тоже ведётся как с задачами. Поговорим об 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. Например: Теперь, когда мы разобрались с тем, что собой представляют asyncio-задачи, поговорим о том, как их создавать. Задачи создают с использованием экземпляра корутины, предоставленного соответствующему механизму. Вспомните — корутину определяют, используя выражение async def. Она выглядит как функция. Например: Задачу можно создать и запланировать на выполнение только внутри корутины. Есть два основных способа создания и планирования задач: Создать объект Task с использованием высокоуровневого API (предпочтительный способ). Создать объект Task с помощью низкоуровневого API. Рассмотрим подробнее каждый из этих способов. Задачу можно создать, прибегнув к функции asyncio.create_task(). Эта функция принимает экземпляр класса корутины и необязательное имя задачи, а возвращает экземпляр класса asyncio.Task. Например: Сделать это можно в одной строке, с помощью сложного выражения. Например: Вот что здесь происходит: Корутина оборачивается в экземпляр Task. Планируется выполнение задачи в текущем цикле событий. Возвращается экземпляр Task. Ссылку на экземпляр Task можно и не сохранять. С задачей можно взаимодействовать посредством методов, её выполнения можно ожидать в корутине. Это — предпочтительный способ создания объектов Task из корутин в asyncio-программах. Задачи можно создавать из корутин и с использованием низкоуровневого API asyncio. Первый способ такого создания задач заключается в использовании функции asyncio.ensure_future(). Эта функция принимает объект Task, Future, или Future-подобный объект, такой, как корутина, и, необязательно — цикл событий, в котором нужно запланировать выполнение соответствующего объекта. Если цикл событий не предоставлен — выполнение объекта будет запланировано в текущем цикле событий. Если этой функции предоставлена корутина — она автоматически оборачивается в экземпляр Task, который и возвращает эта функция. Например: Ещё одна низкоуровневая функция, которую можно использовать для создания объектов Task и для планирования их выполнения, представлена методом loop.create_task(). Этот метод требует доступа к конкретному циклу событий, в котором планируется выполнять корутину как задачу. Можно получить ссылку на экземпляр текущего цикла событий, используемого в asyncio-программе, прибегнув к функции asyncio.get_event_loop(). Затем можно вызвать метод create_task() этого экземпляра цикла для создания экземпляра Task и для планирования его выполнения. Например: Распространённый вопрос о работе с задачами звучит так: «Когда, после того, как задача создана, она запускается?». И это — хороший вопрос. Хотя мы можем планировать независимый запуск корутин в виде задач, пользуясь функцией create_task(), задача может не запуститься немедленно. На самом деле, задача не будет запущена до тех пор, пока у цикла событий не появится возможность её запустить. Это произойдёт тогда, когда все другие корутины перестанут выполняться и настанет очередь интересующей нас задачи. Например, имеется asyncio-программа с одной корутиной, которую создали и выполнение которой, виде задачи, запланировали. Запланированная задача не будет выполнена до тех пор, пока вызывающая корутина, создавшая эту задачу, не будет приостановлена. Это может произойти в том случае, если вызывающая корутина решит приостановить работу, подождать выполнения другой корутины или задачи, или решит подождать выполнения новой задачи, выполнение которой было только что запланировано. Например: На сегодня всё :)  Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки. Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь. Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров. Присоединяйтесь к нашей команде.