Асинхронное программирование на питоне с AsyncIO (для начинающих)

В этой статье я объясню асинхронное программирование на Python с помощью библиотеки AsyncIO.

Прежде чем двигаться дальше, давайте разберемся с терминологией.

Циклы событий: Он используется для одновременного запуска асинхронных задач (которые могут быть сопрограммой, будущим или любыми ожидаемыми объектами). будут выполнены, выполнить их, отложить или отменить

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

Ожидаемые объекты. Мы говорим, что объект является ожидаемым, если его можно использовать в выражении await или yield from.

В python существует три основных типа ожидаемых объектов: сопрограммы, задачи и фьючерсы.

AsyncIO (асинхронный ввод-вывод)

AsyncIO — это библиотека, которая помогает одновременно запускать код с использованием одного потока или цикла событий. В основном она использует async/await API для асинхронного программирования.

AsyncIO был выпущен в python 3.3, до этого мы использовали потоки, гринлет и многопроцессорную библиотеку для достижения асинхронного программирования в python.

Зачем нам нужен AsyncIO для асинхронного программирования?

  1. Поскольку потоки могут использоваться для одновременного выполнения нескольких задач, но потоки Python управляются операционной системой, и ОС должна выполнять больше переключений контекста в черно-белых потоках по сравнению с зелеными потоками.
  2. Greenlet (зеленые потоки) убирает планировщик из ОС и воспроизводит сам планировщик, но CPython по умолчанию не использует зеленые потоки (для этого есть asyncio, gevent, PyPy и др.)
  3. Используя библиотеку многопроцессорности, мы можем создать несколько процессов и позволить программе в полной мере использовать все ядра компьютера, но создание процессов обходится дорого, поэтому для операций ввода-вывода в основном выбираются потоки.

В целом, asyncIO — более читабельный и чистый подход к асинхронному программированию.

Как использовать asyncIO?

Когда asyncIO был выпущен, он использовал декоратор @asyncio.coroutine с сопрограммами на основе генератора для достижения асинхронного программирования.

Сопрограммы генератора Asyncio используют синтаксис yield from для приостановки сопрограммы.

В приведенном ниже примере some_async_task() представляет собой сопрограмму на основе генератора, чтобы выполнить эту сопрограмму, сначала нам нужно получить цикл событий (в строке 11), а затем запланировать выполнение этой задачи в цикле событий, используя (loop.run_until_complete) (Примечание: прямой вызов some_async_task() не будет планировать выполнение этой задачи, она только вернет объект генератора)

Здесь, в строке 8, asyncio.sleep() — это сопрограмма (здесь мы можем использовать любую сопрограмму или tasks/future), и когда инструкция yield from выполнила ее, управление вернется к циклу обработки событий, чтобы позволить выполняться другим сопрограммам, и когда сопрограмма asyncio.sleep() завершится, и когда цикл обработки событий вернет управление сопрограмме some_async_task(), она выполнит дальнейшую инструкцию (например, строку 9)

В Python 3.5 язык представил встроенную поддержку сопрограмм. Теперь мы можем использовать синтаксис async/await для определения собственных сопрограмм.

Метод с префиксом async def автоматически становится собственной сопрограммой.

await можно использовать для получения результата ожидаемых объектов (которые могут быть сопрограммами, задачами или будущими)

How to run the event loop?

До Python 3.7 мы вручную создавали/получали цикл событий, а затем планировали нашу задачу, например:

loop = asyncio.get_event_loop() #if there is no event loop then it will create new one. 
loop.run_until_complete(coroutine()) #run until coroutine is completed.

В python 3.7 и выше ниже приведен предпочтительный способ запуска цикла событий.

asyncio.run(coroutine())
# This function runs the passed coroutine, taking care of managing the asyncio event loop and finalizing asynchronous generators.

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

Futures в Asyncio

Это ожидаемый низкоуровневый объект, который должен иметь результат в будущем.

когда ожидается объект Future, это означает, что сопрограмма будет ждать, пока Future не будет разрешен в каком-то другом месте.

Этот API существует для того, чтобы код на основе обратного вызова можно было использовать с async/await.

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

Задачи в AsyncIO

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

Есть много способов создать задачу:

  • loop.create_task() → через низкоуровневый API и принимает только сопрограммы.
  • asyncio.ensure_future() → через низкоуровневый API, и он может принимать любые ожидаемые объекты, это будет работать на всех версиях Python, но менее читабельно.
  • asyncio.create_task() → через высокоуровневый API и работает на Python 3.7+, принимает сопрограммы и оборачивает их как задачи

asyncio.create_task()

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

В приведенном ниже примере я использую библиотеку aiohttp для получения новостных статей из общедоступных API-интерфейсов хакерских новостей. Я создал две задачи (задача 1 и задача 2) для одновременного получения двух разных новостей и отображения заголовка для обеих новостных статей.

asyncio.ensure_future() похож на asyncio.create_task(), но также может принимать будущее, как показано в приведенном ниже примере.

asyncio.gather(*ожидаемые_объекты, возвращаемые_исключения)

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

Если в каком-либо ожидаемом объекте есть исключение, оно не отменит другие ожидаемые объекты.

В приведенном ниже примере мы запускаем две задачи одновременно, и мы видим, что если в some_async_task2 есть исключение, оно не отменит some_async_task() сопрограмму.

Если return_exceptions имеет значение False и если в каком-либо ожидаемом объекте возникает какое-либо исключение, то await asyncio.gather() немедленно возвращается и показывает ошибку на экране. поэтому для демонстрации в строке 17 мы ожидаем еще одну сопрограмму (которая разрешится через 6 секунд), чтобы убедиться, что программа не завершится через 4 секунды.

И мы можем увидеть выполнение строки 6 в выводе

Если мы хотим собрать весь результат (вместе с исключением) в массив, мы можем использовать return_exceptions=True, который будет обрабатывать исключение как результат, и он будет агрегирован в списке результатов.

На данный момент это все. В будущем я напишу о том, как мы можем использовать эту библиотеку AsyncIO в Django вместе с ASGI.