Главная проблема новичков в асинхронном программировании на Python
Объясняем, в чем проблема асинхронного программирования на Python, и как она касается синтаксиса высокоуровневой концепции async и await.
4К открытий11К показов
Асинхронность в Python должен знать почти каждый разработчик на Python, который хочет быть вовлечён в коммерческую разработку. Как минимум это касается веб-программирования, где очень важна скорость отклика приложения (сервиса). Вообще, асинхронность в Python – это отдельный мир, сильно отличающийся от синхронного Python. Наличие одного событийного цикла уже вносит много изменений в привычный порядок вещей. Существует много интересных и одновременно спорных вещей, суть которых скрыт в самой концепции асинхронности.
Дисклеймер: статья ориентирована на тех, кто хорошо знаком с Python и знает основы асинхронности. Здесь не будут разбираться до мельчайших деталей понятия, используемые в статье для различных пояснений и т.п.
Немного о себе
На данный момент я работаю junior-разработчиком в отечественной IT-компании над сервисом, полностью построенном на aiohttp. Это моя первая работа, да и работаю я сравнительно недолго, так что я не претендую на звание гуру асинхронного программирования. Однако определённый опыт у меня имеется, и хочется поделиться им с читателем.
Источник информации
Тут я порекомендую YouTube-канал Олега Молчанова, на котором есть отличный цикл видео про асинхронность в Python. Именно после его просмотра ко мне пришло базовое понимание данной тематики. Также у него есть небольшой курс на Boosty, в котором, кстати, рассматривается проблема, о которой тут пойдёт речь. Однако ввиду того, что этот контент является платным, я принял решение рассказать о ней тут. Эта проблема будет рассматриваться под призмой моего опыта, так что, если у кого-то есть чем поделиться, жду обратной связи в комментариях.
Непосредственно к проблеме
Итак, наступило время понять, о чём же могут думать не так новички об асинхронности в великом и могучем… Python. А проблема состоит в высокоуровневой концепции async и await. И, таким образом, она разделяется на 2 подпроблемы, качающиеся каждой синтаксической конструкции соответственно.
Что не так с пониманием async
Тут всё просто. Многие думают, что, написав async def, код в функции автоматически становится асинхронным. Это не так. Если этот код спроектирован как синхронный, он таковым и останется. Так, у нас есть две такие функции:
Если запустить его через интерпретатор Python версии от 3.5, мы убедимся, что код выполнился линейно (время выполнения, почти равное 6 секундам, говорит об этом).
Важно помнить, что аббревиатура “asyncio” означает “асинхронный ввод-вывод”. Но даже если эти операции есть в коде, как в нашем случае (time.sleep симулирует операцию ввода), недостаточно написать async def. Помимо этого, необходимо использовать асинхронный аналог функции. Например, для time.sleep это asyncio.sleep.
Если бы мы использовали асинхронный аналог паузы в программе, время выполнения бы было равно величине самой большой паузы, а именно: 5 секундам. Попробуйте поменять time.sleep на await asyncio.sleep, и убедитесь в этом сами.
Тогда зачем нужен async, если его добавление, на первый взгляд, ничего не делает? На самом деле, делает. Под капотом у такой функции происходит процесс, инициализирующий из неё корутину, которую впоследствии можно будет поместить в специальный класс Task, который, в свою очередь, будет помещён цикл событий.
Корутина – это подвид генераторов, который может как принимать данные от других корутин, так и отдавать их. Обычные генераторы могут только генерировать данные, что и следует из их названия.
Краткий вывод по async
Если вы не понимаете тонкостей работы корутин, не задумывайтесь о них и просто ставьте это ключевое слово к той функции, где планируете использовать асинхронный код. Это обязательное условие в мире асинхронного программирования на Python.
Однако прописывать его везде, где только можно, не очень хорошая идея: в эффективности код не прибавит (даже наоборот, из-за накладных действий для инициализации корутины).
Что не так с пониманием await
Эта проблема, по моему мнению, больше вводит в заблуждение, чем первая, и сейчас я постараюсь объяснить, почему. Но сперва скажу, в чём она заключается: многим новичкам может показаться, что await переключает контроль выполнения задач (корутин) в событийный цикл. На самом деле, await просто делегирует определённые действия в другую корутину, а переключением контроля выполнения занимается yield, приостанавливающий выполнение корутины и отдающий какое-то значение.
Не надо бояться слова “делегировать”. Оно означает передача ответственности за какие-либо действия кому-то другому. В программировании это означает, что мы можем поместить определённую часть кода в отдельную функцию, и вызывать её из другой функции, таким образом, делегируя определённые действия в неё. Такой же принцип и с корутинами и генераторами.
Давайте убедимся в этом на практике:
Если бы await отдавал контроль выполнения событийному циклу, программа напечатала бы “hello ! world”, но тут код выполнился последовательно (по порядку, указанному в gather). Всё потому, что await просто вызывает корутину, “проваливается” внутрь её. Стоит отметить, что выполнение корутины, из которой производится await, приостанавливает своё выполнение, ожидая (“wait” с английского), когда выполнится “дочерняя” кореутина. А вот в корутине уже могут быть yield, которые дают шанс выполниться другим задачам в событийном цикле.
Опять же, поменяв time.sleep на асинхронный аналог, всё встанет на свои места. И тут важно понять: в своей реализации asyncio.sleep использует yield, и именно он является главным фактором в той магии, которая сокращает время выполнения программы и конкурентного выполнения корутин.
Краткий вывод по await
Всё просто. Проводя аналогии с синхронным Python, если корутина – это функция, то await – это вызов функции (эквивалентно скобкам после названия функции). Всё, ничего более. За конкурентность отвечают другие языковые конструкции.
Время выводов
В некоторых материалах и видео на YouTube я наблюдал следующие тезисы: “Если ты не знаешь, как писать асинхронный код, просто ставь везде async и await”. И я скажу так: если нужно написать единичный скрипт на Python и это вообще не является твоей профильной сферой, то можно не погружаться в подробности и делать согласно этому тезису. Однако в остальных случаях на такой идеологии далеко не уедешь. Важно понимать, как работает асинхронность под капотом хотя бы на концептуальном уровне, чтобы уметь проектировать асинхронные программы. А async/await – это просто высокоуровневые обёртки, которыми легко пользоваться тем, кто понимает, как они работают. Я постарался раскрыть главную проблему новичков в этом направлении, поверхностно разобрав каждую конструкцию. Надеюсь, что эта статья помогла разобраться в этой концепции и что я направил вас в нужное русло в плане развития навыков в асинхронном программировании на Python. Всем удачи и спасибо за то, что прочитали до конца!
4К открытий11К показов