Содержание
Памятка: ключевые шаги для запуска async функций
- Определите асинхронную функцию с помощью async def.
- Используйте await внутри функции для вызова других корутин.
- Для запуска из синхронного кода используйте asyncio.run().
- Для параллельного выполнения задач применяйте asyncio.gather().
- Создавайте задачи через asyncio.create_task() для фонового выполнения.
- Не забывайте обрабатывать исключения внутри корутин.
- Избегайте блокирующих операций (time.sleep) внутри async функций.
- Используйте asyncio.sleep() для неблокирующих задержек.
- Проверяйте версию Python (3.5+ для async/await).
- Для отладки включайте режим debug в asyncio.run().
- Изучите asyncio.Queue для обмена данными между задачами.
- Помните, что корутины выполняются только внутри цикла событий.
Почему так сложно понять asyncio
Асинхронное программирование традиционно относят к темам для «продвинутых». Действительно, у новичков часто возникают сложности с практическим освоением асинхронности. В случае python на то есть весьма веские причины:
- Асинхронность в python была стандартизирована сравнительно недавно. Библиотека asyncio появилась впервые в версии 3.5 (то есть в 2015 году), хотя возможность костыльно писать асинхронные приложения и даже фреймворки, конечно, была и раньше. Соответственно у Лутца она не описана, а, как всем известно, «чего у Лутца нет, того и знать не надо».
- Рекомендуемый синтаксис асинхронных команд неоднократно менялся уже и после первого появления asyncio. В сети бродит огромное количество статей и роликов, использующих архаичный код различной степени давности, только сбивающий новичков с толку.
- Официальная документация 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
Сюрприз! Мгновенно выполнились быстрые части обеих функций и затем через 3 секунды (3, а не 6!) одновременно появились оба текстовых сообщения. Полное ощущение, что функции выполнились параллельно (на самом деле нет).
А можно аналогичным образом добавить еще одну функцию-соню? Пожалуйста — хоть сто! Общее время выполнения программы будет по-прежнему определяться самой «тормознутой» из них. Добро пожаловать в асинхронный мир!
- Перед определениями функций появился префикс async. Он говорит интерпретатору, что функция должна выполняться асинхронно.
- Вместо привычного мы использовали. Это «неблокирующий sleep». В рамках функции ведет себя так же, как традиционный, но не останавливает интерпретатор в целом.
- Перед вызовом асинхронных функций появился префикс await. Он говорит интерпретатору примерно следующее: «я тут возможно немного потуплю, но ты меня не жди — пусть выполняется другой код, а когда у меня будет настроение продолжиться, я тебе маякну».
- На базе функций мы при помощи asyncio.create_task создали задачи (что это такое разберем позже) и запустили все это при помощи
Перед определениями функций появился префикс async. Он говорит интерпретатору, что функция должна выполняться асинхронно.
Вместо привычного мы использовали. Это «неблокирующий sleep». В рамках функции ведет себя так же, как традиционный, но не останавливает интерпретатор в целом.
Перед вызовом асинхронных функций появился префикс await. Он говорит интерпретатору примерно следующее: «я тут возможно немного потуплю, но ты меня не жди — пусть выполняется другой код, а когда у меня будет настроение продолжиться, я тебе маякну».
На базе функций мы при помощи asyncio.create_task создали задачи (что это такое разберем позже) и запустили все это при помощи
Пример 2.2
Ага, значит задача (что бы это ни значило) имеет тип <class ‘_asyncio.Task’>. Привет, капитан Очевидность!
А кто ваша мама, ребята? А мама наша — анархия какая-то еще более загадочная футура (<class ‘_asyncio.Future’>).
В asyncio все шиворот-навыворот, поэтому сначала выясним что такое футура (которую мы видим впервые в жизни), а потом разберемся с ее дочкой задачей (с которой мы уже имели честь познакомиться в предыдущем разделе).
Футура (если совсем упрощенно) — это оболочка для некой асинхронной сущности, позволяющая выполнять ее «как бы одновременно» с другими асинхронными сущностями, переключаясь от одной сущности к другой в точках, обозначенных ключевым словом await
Кроме того футура имеет внутреннюю переменную «результат», которая доступна через.result() и устанавливается через.set_result(value). Пока ничего не надо делать с этим знанием, оно пригодится в дальнейшем.
У футуры на самом деле еще много чего есть внутри, но на данном этапе не будем слишком углубляться. Футуры в чистом виде используются в основном разработчиками фреймворков, нам же для разработки приложений приходится иметь дело с их дочками — задачами.
Задача — это частный случай футуры, предназначенный для оборачивания корутины.
Как это работает:
- выполнилась быстрая часть функции fun1
- fun1 сказала интерпретатору «иди дальше, я посплю 3 секунды»
- выполнилась быстрая часть функции fun2
- fun2 сказала интерпретатору «иди дальше, я посплю 3 секунды»
- интерпретатору дальше делать нечего, поэтому он ждет пока ему маякнет первая проснувшаяся функция
- на доли миллисекунды раньше проснулась fun1 (она ведь и уснула чуть раньше) и отрапортовала нам об успешном завершении
- то же самое сделала функция fun2
интерпретатору дальше делать нечего, поэтому он ждет пока ему маякнет первая проснувшаяся функция
на доли миллисекунды раньше проснулась fun1 (она ведь и уснула чуть раньше) и отрапортовала нам об успешном завершении
Замените «посплю» на «пошлю запрос удаленному сервису и буду ждать ответа» и вы поймете как работает реальное асинхронное приложение.
Пример 2.3
Уже интереснее! Класс функции не изменился, но благодаря ключевому слову 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 и это прекрасно. Кому интересны детали, рекомендую пройти вот сюда.
Асинхронные функции и корутины
Теперь давайте немного разберемся с типами. Вернемся к «неасинхронному» примеру 2.1, слегка модифицировав его:
Пример 3.2
«И говорит по радио товарищ Левитан: в Москве погода ясная, а в Лондоне — туман!» (c) Е.Соев
Внимание! Если будет слишком много желающих потестить сервис с моим ключом, его могут временно заблокировать. В этом случае просто получите свой собственный, это быстро и бесплатно.
Обратите внимание, мы использовали два вложенных менеджера контекста: для сессии и для функции get. Так требует документация aiohttp, не будем с ней спорить.
Давайте попробуем реализовать тот же самый функционал, используя классическую синхронную библиотеку requests и сравним скорость:
Футуры и задачи
Продолжим исследовать нашу программу из примера 2.2. Помнится, на базе корутин мы там создали какие-то загадочные задачи:
Пример 4.1
Результат тот же самый, но появилось упоминание о каком-то непонятном цикле событий (event loop) и вместо одной asyncio.runиспользуются аж три функции:, asyncio.get_event_loop и asyncio.run_until_complete. Кроме того, если вы используете python версии 3.10+, в консоль прилетает раздражающее предупреждение DeprecationWarning: There is no current event loop, что само по себе наводит на мысль, что мы делаем что-то слегка не так.
Давайте пока руководствоваться Дзен питона: «Простое лучше, чем сложное», а цикл событий сам придет к нам… в свое время.
Все трагически усложняется
Вернемся к примеру 2.2 и опишем его логику заново, используя теперь уже знакомые нам термины — корутины и задачи:
- корутину асинхронной функции fun1 обернули задачей task1
- корутину асинхронной функции fun2 обернули задачей task2
- в асинхронной функции main обозначили точку переключения к задаче task1
- в асинхронной функции main обозначили точку переключения к задаче task2
- корутину асинхронной функции main передали в функцию
в асинхронной функции main обозначили точку переключения к задаче task1
в асинхронной функции main обозначили точку переключения к задаче task2
Бр-р-р, ужас какой… Воистину: «Во многой мудрости много печали; и кто умножает познания, умножает скорбь» (Еккл. 1:18)
Все счастливо упрощается
А можно проще? Ведь понятие корутина нам необходимо, только чтобы отличать функцию от результата ее выполнения. Давайте попробуем временно забыть про них. Попробуем также перефразировать неуклюжие «точки переключения» и вот эти вот все «обернули-передали». Кроме того, поскольку — это единственная рекомендованная точка входа в приложение для python 3.8+, ее отдельное упоминание тоже совершенно излишне для понимания логики нашего приложения.
А теперь (барабанная дробь)… Мы вообще уберем из кода все упоминания об асинхронности. Я понимаю, что работать не будет, но все же давайте посмотрим что получится:
Пример 4.2 (не работающий)
Кощунство, скажете вы? Нет, я всего лишь честно выполняю рекомендацию великого и ужасного Гвидо ван Россума:
Звучит почти как: «Наденьте зеленые очки и притворитесь, что стекляшки — это изумруды»
Задачи — это «ракеты-носители» для конкурентного запуска «боеголовок»-функций.
А если вообще без задач?
Как это? Ну вот так, ни в какие задачи ничего не заворачивать, а просто эвейтнуть в main() сами корутины. А что, имеем право!
Пример 4.3 (неудачный)
Грусть-печаль… Снова 6 секунд как в давнем примере 1.1, ни разу не асинхронном. Боеголовка без ракеты взлетать отказалась.
Пара слов о конкурентности
С точки зрения разработчика и (особенно) пользователя конкурентное выполнение в асинхронных и многопоточных приложениях выглядит почти как параллельное. На самом деле никакого параллельного выполнения чего бы то ни было в питоне нет и быть не может. Кто не верит — погулите аббревиатуру GIL. Именно поэтому мы используем осторожное выражение «конкурентное выполнение задач» вместо «параллельное».
Нет, конечно, если очень хочется настоящего параллелизма, можно запустить несколько интерпретаторов python одновременно (библиотека multiprocessing фактически так и делает). Но без крайней нужды лучше такими вещами не заниматься, ибо издержки чаще всего будут непропорционально велики по сравнению с профитом.
А что есть «крайняя нужда»? Это приложения-числодробилки. В них подавляющая часть времени выполнения расходуется на операции процессора и обращения к памяти. Никакого ленивого ожидания ответа от медленной периферии, только жесткий математический хардкор. В этом случае вас, конечно, не спасет ни изящная асинхронность, ни неуклюжая мультипоточность. К счастью, такие негуманные приложения в практике веб-разработки встречаются нечасто.
Асинхронные менеджеры контекста и настоящее асинхронное приложение
Пришло время написать на asyncio не тупой перебор неблокирующих слипов, а что-то выполняющее действительно осмысленную работу. Но прежде чем приступить, разберемся с асинхронными менеджерами контекста.
Если вы умеете работать с обычными менеджерами контекста, то без труда освоите и асинхронные. Тут используется знакомая конструкция with, только с префиксом async, и те же самые контекстные методы, только с буквой a в начале.
Пример 5.1
Работает превосходно, но… В среднем занимает 2-3 секунды, то есть раз в 10 больше чем в асинхронном примере. Что и требовалось доказать.
А может ли асинхронная функция не просто что-то делать внутри себя (например, запрашивать и выводить в консоль погоду), но и возвращать результат? Ту же погоду, например, чтобы дальнейшей обработкой занималась функция верхнего уровня main().
Нет ничего проще. Только в этом случае для группового запуска задач необходимо использовать уже не цикл с await, а функцию
Пример 5.2
Никаких сюрпризов — fun2 честно ждет пока полностью отработает fun1 (и быстрая ее часть, и медленная) и только потом начинает выполнять свою работу. Весь процесс занимает 3 + 3 = 6 секунд. Строго говоря, чуть больше чем 6 за счет «быстрых» арифметических операций, но в выбранном масштабе разницу уловить невозможно.
Теперь попробуем сделать то же самое, но в асинхронном режиме. Пока просто запустите предложенный код, подробно мы его разберем чуть позже.
Пример 5.3
Создавать свои асинхронные менеджеры контекста разработчику приложений приходится нечасто, а вот использовать готовые из асинхронных библиотек — постоянно. Поэтому нам полезно знать, что находится у них внутри.
Теперь, зная как работают асинхронные менеджеры контекста, можно написать ну очень полезное приложение, которое узнает погоду в разных городах при помощи библиотеки aiohttp и API-сервиса:
Пример 5.4
Красиво получилось! Обратите внимание, мы использовали выражение со звездочкой *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.


























