Запуск асинхронной функции Python: asyncio, корутины и футуры

0
34

Памятка: ключевые шаги для запуска async функций

  1. Определите асинхронную функцию с помощью async def.
  2. Используйте await внутри функции для вызова других корутин.
  3. Для запуска из синхронного кода используйте asyncio.run().
  4. Для параллельного выполнения задач применяйте asyncio.gather().
  5. Создавайте задачи через asyncio.create_task() для фонового выполнения.
  6. Не забывайте обрабатывать исключения внутри корутин.
  7. Избегайте блокирующих операций (time.sleep) внутри async функций.
  8. Используйте asyncio.sleep() для неблокирующих задержек.
  9. Проверяйте версию Python (3.5+ для async/await).
  10. Для отладки включайте режим debug в asyncio.run().
  11. Изучите asyncio.Queue для обмена данными между задачами.
  12. Помните, что корутины выполняются только внутри цикла событий.

Почему так сложно понять asyncio

asyncio in - изображение номер один
asyncio in — изображение номер один

Асинхронное программирование традиционно относят к темам для «продвинутых». Действительно, у новичков часто возникают сложности с практическим освоением асинхронности. В случае python на то есть весьма веские причины:

  1. Асинхронность в python была стандартизирована сравнительно недавно. Библиотека asyncio появилась впервые в версии 3.5 (то есть в 2015 году), хотя возможность костыльно писать асинхронные приложения и даже фреймворки, конечно, была и раньше. Соответственно у Лутца она не описана, а, как всем известно, «чего у Лутца нет, того и знать не надо».
  2. Рекомендуемый синтаксис асинхронных команд неоднократно менялся уже и после первого появления asyncio. В сети бродит огромное количество статей и роликов, использующих архаичный код различной степени давности, только сбивающий новичков с толку.
  3. Официальная документация asyncio (разумеется, исчерпывающая и прекрасно организованная) рассчитана скорее на создателей фреймворков, чем на разработчиков пользовательских приложений. Там столько всего — глаза разбегаются. А между тем: «Вам нужно знать всего около семи функций для использования asyncio» (c) Юрий Селиванов, автор PEP 492, в которой были добавлены инструкции async и await

Асинхронность в python была стандартизирована сравнительно недавно. Библиотека asyncio появилась впервые в версии 3.5 (то есть в 2015 году), хотя возможность костыльно писать асинхронные приложения и даже фреймворки, конечно, была и раньше. Соответственно у Лутца она не описана, а, как всем известно, «чего у Лутца нет, того и знать не надо».

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

Официальная документация asyncio (разумеется, исчерпывающая и прекрасно организованная) рассчитана скорее на создателей фреймворков, чем на разработчиков пользовательских приложений. Там столько всего — глаза разбегаются. А между тем: «Вам нужно знать всего около семи функций для использования asyncio» (c) Юрий Селиванов, автор PEP 492, в которой были добавлены инструкции async и await

Иногда по выходным мы с собакой выезжаем на рыбалку. Добравшись до берега, я снаряжаю и забрасываю несколько донок с колокольчиками. И… Переключаюсь на другие задачи: разговариваю с собакой, любуюсь красотами природы, истребляю на себе комаров. Я не думаю о рыбе. Задачи «поймать рыбу удочкой N» находятся в режиме ожидания. Когда рыба будет готова к общению, одна из удочек сама даст о себе знать звонком колокольчика.

Будь я автором самого толстого в мире учебника по python, я бы рассказывал читателям про асинхронное программирование уже с первых страниц. Вот только написали «Hello, world!» и тут же приступили к созданию «Hello, asynchronous world!». А уже потом циклы, условия и все такое.

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

Пара слов о терминологии

Асинхронный код на - изображение номер два
Асинхронный код на — изображение номер два

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

Внимание! Все примеры отлажены в консольном python 3.10. Вероятно в ближайших последующих версиях также работать будут. Однако обратной совместимости со старыми версиями не гарантирую. Если у вас что-то пошло не так, попробуйте, установить 3.10 и/или не пользоваться Jupyter.

Первое асинхронное приложение

Асинхронное программирование на примере - изображение номер три
Асинхронное программирование на примере — изображение номер три

Предположим, у нас есть две функции в каждой из которых есть некая «быстрая» операция (например, арифметическое вычисление) и «медленная» операция ввода/вывода. Детали реализации медленной операции нам сейчас не важны. Будем моделировать ее функцией (). Наша задача — выполнить обе задачи как можно быстрее.

Пример 2.1

How and - изображение номер четыре
How and — изображение номер четыре

Understanding python async with - изображение номер пять
Understanding python async with — изображение номер пять

Сюрприз! Мгновенно выполнились быстрые части обеих функций и затем через 3 секунды (3, а не 6!) одновременно появились оба текстовых сообщения. Полное ощущение, что функции выполнились параллельно (на самом деле нет).

А можно аналогичным образом добавить еще одну функцию-соню? Пожалуйста — хоть сто! Общее время выполнения программы будет по-прежнему определяться самой «тормознутой» из них. Добро пожаловать в асинхронный мир!

  1. Перед определениями функций появился префикс async. Он говорит интерпретатору, что функция должна выполняться асинхронно.
  2. Вместо привычного мы использовали. Это «неблокирующий sleep». В рамках функции ведет себя так же, как традиционный, но не останавливает интерпретатор в целом.
  3. Перед вызовом асинхронных функций появился префикс await. Он говорит интерпретатору примерно следующее: «я тут возможно немного потуплю, но ты меня не жди — пусть выполняется другой код, а когда у меня будет настроение продолжиться, я тебе маякну».
  4. На базе функций мы при помощи asyncio.create_task создали задачи (что это такое разберем позже) и запустили все это при помощи
ЧИТАТЬ ТАКЖЕ:  Список чисел в Python с остатком 3 при делении на 5

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

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

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

На базе функций мы при помощи asyncio.create_task создали задачи (что это такое разберем позже) и запустили все это при помощи

Пример 2.2

Git - изображение номер шесть
Git — изображение номер шесть

Python - изображение номер семь
Python — изображение номер семь

Ага, значит задача (что бы это ни значило) имеет тип <class ‘_asyncio.Task’>. Привет, капитан Очевидность!

А кто ваша мама, ребята? А мама наша — анархия какая-то еще более загадочная футура (<class ‘_asyncio.Future’>).

В asyncio все шиворот-навыворот, поэтому сначала выясним что такое футура (которую мы видим впервые в жизни), а потом разберемся с ее дочкой задачей (с которой мы уже имели честь познакомиться в предыдущем разделе).

Футура (если совсем упрощенно) — это оболочка для некой асинхронной сущности, позволяющая выполнять ее «как бы одновременно» с другими асинхронными сущностями, переключаясь от одной сущности к другой в точках, обозначенных ключевым словом await

Кроме того футура имеет внутреннюю переменную «результат», которая доступна через.result() и устанавливается через.set_result(value). Пока ничего не надо делать с этим знанием, оно пригодится в дальнейшем.

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

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

Как это работает:

Async - изображение номер восемь
Async — изображение номер восемь
  • выполнилась быстрая часть функции fun1
  • fun1 сказала интерпретатору «иди дальше, я посплю 3 секунды»
  • выполнилась быстрая часть функции fun2
  • fun2 сказала интерпретатору «иди дальше, я посплю 3 секунды»
  • интерпретатору дальше делать нечего, поэтому он ждет пока ему маякнет первая проснувшаяся функция
  • на доли миллисекунды раньше проснулась fun1 (она ведь и уснула чуть раньше) и отрапортовала нам об успешном завершении
  • то же самое сделала функция fun2

интерпретатору дальше делать нечего, поэтому он ждет пока ему маякнет первая проснувшаяся функция

на доли миллисекунды раньше проснулась fun1 (она ведь и уснула чуть раньше) и отрапортовала нам об успешном завершении

Замените «посплю» на «пошлю запрос удаленному сервису и буду ждать ответа» и вы поймете как работает реальное асинхронное приложение.

Пример 2.3

python async function without await - изображение номер девять
python async function without await — изображение номер девять

Презентация \ - изображение номер десять
Презентация \ — изображение номер десять

Уже интереснее! Класс функции не изменился, но благодаря ключевому слову async она теперь возвращает не <class ‘NoneType’>, а <class ‘coroutine’>. Ничто превратилось в нечто! На сцену выходит новая сущность — корутина.

Что нам нужно знать о корутине? На начальном этапе немного. Помните как в python устроен генератор? Ну, это то, что функция начинает возвращать, если в нее добавить yield вместо return. Так вот, корутина — это разновидность генератора.

Далее по тексту мы постараемся придерживаться правильной терминологии: асинхронная (или корутинная) функция — это f, а корутина — f(). Но если вы в разговоре назовете корутиной асинхронную функцию, беды большой не произойдет, вас поймут. «Не важно, какого цвета кошка, лишь бы она ловила мышей» (с) тов. Дэн Сяопин

Пара слов о "медленных" операциях

Асинхронность в - изображение номер одиннадцать
Асинхронность в — изображение номер одиннадцать

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

Однако следует четко понимать: для эффективного использования с asyncio любой медленный интерфейс должен поддерживать асинхронные функции. Иначе никакого выигрыша в производительности вы не получите. Попробуйте использовать в примере 2.2 вместо и вы поймете о чем я.

Что касается http-запросов, то здесь есть великолепная библиотека aiohttp, честно реализующая асинхронный доступ к веб-серверу. С файловыми операциями сложнее. В Linux доступ к файловой системе по определению не асинхронный, поэтому, несмотря на наличие удобной библиотеки aiofiles, где-то в ее глубине всегда будет иметь место многопоточный «мостик» к низкоуровневым функциям ОС. С доступом к БД примерно то же самое. Вроде бы, последние версии SQLAlchemy поддерживают асинхронный доступ, но что-то мне подсказывает, что в основе там все тот же старый добрый Threadpool. С другой стороны, в веб-приложениях львиная доля задержек относится именно к сетевому общению, так что «не вполне асинхронный» доступ к локальным ресурсам обычно не является бутылочным горлышком.

Внимательные читатели меня поправили в комментах. В Linux, начиная с ядра 5.1, есть полноценный асинхронный интерфейс io_uring и это прекрасно. Кому интересны детали, рекомендую пройти вот сюда.

Асинхронные функции и корутины

Part 2 - изображение номер двенадцать
Part 2 — изображение номер двенадцать

Теперь давайте немного разберемся с типами. Вернемся к «неасинхронному» примеру 2.1, слегка модифицировав его:

Пример 3.2

Асинхронное программирование на - изображение номер тринадцать
Асинхронное программирование на — изображение номер тринадцать

И говорит по радио товарищ - изображение номер четырнадцать
И говорит по радио товарищ — изображение номер четырнадцать

«И говорит по радио товарищ Левитан: в Москве погода ясная, а в Лондоне — туман!» (c) Е.Соев

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

Обратите внимание, мы использовали два вложенных менеджера контекста: для сессии и для функции get. Так требует документация aiohttp, не будем с ней спорить.

Давайте попробуем реализовать тот же самый функционал, используя классическую синхронную библиотеку requests и сравним скорость:

Футуры и задачи

Python how to build python scheduler async program from scratch - изображение номер пятнадцать
Python how to build python scheduler async program from scratch — изображение номер пятнадцать

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

Пример 4.1

Принципы работы с async/await в - изображение номер шестнадцать
Принципы работы с async/await в — изображение номер шестнадцать

Публикация #2026 - изображение номер семнадцать
Публикация #2026 — изображение номер семнадцать

Результат тот же самый, но появилось упоминание о каком-то непонятном цикле событий (event loop) и вместо одной asyncio.runиспользуются аж три функции:, asyncio.get_event_loop и asyncio.run_until_complete. Кроме того, если вы используете python версии 3.10+, в консоль прилетает раздражающее предупреждение DeprecationWarning: There is no current event loop, что само по себе наводит на мысль, что мы делаем что-то слегка не так.

Давайте пока руководствоваться Дзен питона: «Простое лучше, чем сложное», а цикл событий сам придет к нам… в свое время.

Все трагически усложняется

Coroutine (корутины): что это за сопрограммы, как работают на - изображение номер восемнадцать
Coroutine (корутины): что это за сопрограммы, как работают на — изображение номер восемнадцать

Вернемся к примеру 2.2 и опишем его логику заново, используя теперь уже знакомые нам термины — корутины и задачи:

  • корутину асинхронной функции fun1 обернули задачей task1
  • корутину асинхронной функции fun2 обернули задачей task2
  • в асинхронной функции main обозначили точку переключения к задаче task1
  • в асинхронной функции main обозначили точку переключения к задаче task2
  • корутину асинхронной функции main передали в функцию
ЧИТАТЬ ТАКЖЕ:  Что делает метод Pop в Python: удаление и получение элемента списка

в асинхронной функции main обозначили точку переключения к задаче task1

в асинхронной функции main обозначили точку переключения к задаче task2

Бр-р-р, ужас какой… Воистину: «Во многой мудрости много печали; и кто умножает познания, умножает скорбь» (Еккл. 1:18)

Все счастливо упрощается

Пин от пользователя - изображение номер девятнадцать
Пин от пользователя — изображение номер девятнадцать

А можно проще? Ведь понятие корутина нам необходимо, только чтобы отличать функцию от результата ее выполнения. Давайте попробуем временно забыть про них. Попробуем также перефразировать неуклюжие «точки переключения» и вот эти вот все «обернули-передали». Кроме того, поскольку — это единственная рекомендованная точка входа в приложение для python 3.8+, ее отдельное упоминание тоже совершенно излишне для понимания логики нашего приложения.

А теперь (барабанная дробь)… Мы вообще уберем из кода все упоминания об асинхронности. Я понимаю, что работать не будет, но все же давайте посмотрим что получится:

Пример 4.2 (не работающий)

F-35I - изображение номер двадцать
F-35I — изображение номер двадцать

График работы) - изображение номер двадцать один
График работы) — изображение номер двадцать один

Кощунство, скажете вы? Нет, я всего лишь честно выполняю рекомендацию великого и ужасного Гвидо ван Россума:

Звучит почти как: «Наденьте зеленые очки и притворитесь, что стекляшки — это изумруды»

Задачи — это «ракеты-носители» для конкурентного запуска «боеголовок»-функций.

А если вообще без задач?

Задача, которую не могут решить 90% людей - изображение номер двадцать два
Задача, которую не могут решить 90% людей — изображение номер двадцать два

Как это? Ну вот так, ни в какие задачи ничего не заворачивать, а просто эвейтнуть в main() сами корутины. А что, имеем право!

Пример 4.3 (неудачный)

Mastering - изображение номер двадцать три
Mastering — изображение номер двадцать три

Лента - изображение номер двадцать четыре
Лента — изображение номер двадцать четыре

Грусть-печаль… Снова 6 секунд как в давнем примере 1.1, ни разу не асинхронном. Боеголовка без ракеты взлетать отказалась.

Пара слов о конкурентности

Прокачиваем асинхронное программирование на - изображение номер двадцать пять
Прокачиваем асинхронное программирование на — изображение номер двадцать пять

С точки зрения разработчика и (особенно) пользователя конкурентное выполнение в асинхронных и многопоточных приложениях выглядит почти как параллельное. На самом деле никакого параллельного выполнения чего бы то ни было в питоне нет и быть не может. Кто не верит — погулите аббревиатуру GIL. Именно поэтому мы используем осторожное выражение «конкурентное выполнение задач» вместо «параллельное».

Нет, конечно, если очень хочется настоящего параллелизма, можно запустить несколько интерпретаторов python одновременно (библиотека multiprocessing фактически так и делает). Но без крайней нужды лучше такими вещами не заниматься, ибо издержки чаще всего будут непропорционально велики по сравнению с профитом.

А что есть «крайняя нужда»? Это приложения-числодробилки. В них подавляющая часть времени выполнения расходуется на операции процессора и обращения к памяти. Никакого ленивого ожидания ответа от медленной периферии, только жесткий математический хардкор. В этом случае вас, конечно, не спасет ни изящная асинхронность, ни неуклюжая мультипоточность. К счастью, такие негуманные приложения в практике веб-разработки встречаются нечасто.

Асинхронные менеджеры контекста и настоящее асинхронное приложение

Асинхронный python без головной боли (часть 1) / - изображение номер двадцать шесть
Асинхронный python без головной боли (часть 1) / — изображение номер двадцать шесть

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

Если вы умеете работать с обычными менеджерами контекста, то без труда освоите и асинхронные. Тут используется знакомая конструкция with, только с префиксом async, и те же самые контекстные методы, только с буквой a в начале.

Пример 5.1

Паттерн - изображение номер двадцать семь
Паттерн — изображение номер двадцать семь

Много - изображение номер двадцать восемь
Много — изображение номер двадцать восемь

Работает превосходно, но… В среднем занимает 2-3 секунды, то есть раз в 10 больше чем в асинхронном примере. Что и требовалось доказать.

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

Нет ничего проще. Только в этом случае для группового запуска задач необходимо использовать уже не цикл с await, а функцию

Пример 5.2

Решение всех математических задач школьника / - изображение номер двадцать девять
Решение всех математических задач школьника / — изображение номер двадцать девять

Two babies one fox full comic - изображение номер тридцать
Two babies one fox full comic — изображение номер тридцать

Никаких сюрпризов — fun2 честно ждет пока полностью отработает fun1 (и быстрая ее часть, и медленная) и только потом начинает выполнять свою работу. Весь процесс занимает 3 + 3 = 6 секунд. Строго говоря, чуть больше чем 6 за счет «быстрых» арифметических операций, но в выбранном масштабе разницу уловить невозможно.

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

Пример 5.3

Python 3 - изображение номер тридцать один
Python 3 — изображение номер тридцать один

Асинхронное программирование - изображение номер тридцать два
Асинхронное программирование — изображение номер тридцать два

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

Теперь, зная как работают асинхронные менеджеры контекста, можно написать ну очень полезное приложение, которое узнает погоду в разных городах при помощи библиотеки aiohttp и API-сервиса:

Пример 5.4

составь примеры по рисунку - изображение номер тридцать три
составь примеры по рисунку — изображение номер тридцать три

Примеры на 5 - презентация онлайн - изображение номер тридцать четыре
Примеры на 5 — презентация онлайн — изображение номер тридцать четыре

Красиво получилось! Обратите внимание, мы использовали выражение со звездочкой *tasks для распаковки списка задач в аргументы функции.

Пара слов о лишних сущностях

Кажется, я совершил невозможное. Настучал уже почти тысячу строк текста и ни разу не упомянул о цикле событий. Ну, почти ни разу. Один раз все-же упомянул: в примере 2.3 «как не надо делать». А между тем, в традиционных руководствах по asyncio этим самым циклом событий начинают душить несчастного читателя буквально с первой страницы. На самом деле цикл событий в наших программах присутствует, но он надежно скрыт от посторонних глаз высокоуровневыми конструкциями. До сих пор у нас не возникало в нем нужды, вот и я и не стал плодить лишних сущностей, руководствуясь принципом дорогого товарища Оккама.

Но рано или поздно жизнь заставит нас извлечь этот скелет из шкафа и рассмотреть его во всех подробностях.

Часто задаваемые вопросы о запуске async функций в Python

Вопрос: Как запустить async функцию из обычного синхронного кода?
Ответ: Используйте asyncio.run(your_async_function()).

Вопрос: Что такое корутина в Python?
Ответ: Это асинхронная функция, объявленная с ключевым словом async def, которая возвращает объект корутины.

Вопрос: В чем разница между asyncio.run() и event_loop.run_until_complete()?
Ответ: asyncio.run() — это высокоуровневый API, который создает и закрывает цикл событий автоматически, в то время как run_until_complete требует ручного управления циклом.

Вопрос: Можно ли запустить несколько async функций одновременно?
Ответ: Да, используйте asyncio.gather() или asyncio.create_task() для параллельного выполнения.

Вопрос: Что такое asyncio.create_task()?
Ответ: Это функция, которая оборачивает корутину в задачу (Task) и планирует её выполнение в цикле событий.

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

Вопрос: Как обрабатывать исключения в async функциях?
Ответ: Используйте try/except внутри корутины или обрабатывайте исключения при вызове await.

Вопрос: Что такое asyncio.sleep() и чем он отличается от time.sleep()?
Ответ: asyncio.sleep() — неблокирующая задержка, которая позволяет выполняться другим задачам, в то время как time.sleep() блокирует весь поток.

Вопрос: Можно ли использовать async/await в старых версиях Python?
Ответ: Нет, async/await доступны только с Python 3.5 и выше.

Вопрос: Как отлаживать async код?
Ответ: Используйте asyncio.run() с параметром debug=True или логирование с помощью модуля logging.