Хм, в неделю уложиться все равно не получилось, зато на прошлых выходных немного поковырял aiohttp, написал небольшое API для задачки. Потихоньку вгрызаюсь в асинхронный код и веб фреймворки) Ловите 2 часть перевода материала по асинхронному программированию с сайта realpython.com.
Первая часть тут
__________________________________________________________________________________________
Библиотека asyncio и await/async.
Теперь, когда у вас есть общее представление о асинхронности, как концепции, давайте рассмотрим реализацию в Python. За написание асинхронного кода тут отвечает библиотека asyncio (которая была представлена в версии 3.4) и ключевые слова async and await.
Async/await команды и натуральные корутины
Слова предостережения: Будьте осторожны, когда читаете мануалы. Async io api в Python очень стремительно изменялась между версиями 3.4 -> 3.7. Некоторые паттерны уже устарели и больше не используются, а некоторые запрещенные в первых версиях вещи наоборот разрешены. Этот туториал так же устареет достаточно быстро.
Сердце асинхронного программирования - это корутины. Корутины - это специальная версия функций-генераторов в Python. Начнем с базовых определений и будем постепенно их усложнять: корутина - это функция, которая может прерывать свое исполнение перед тем, как вернет какое-то значение и одновременно передавать контроль другой корутине.
Чуть позже, мы с вами глубже разберемся, как именно традиционный генератор был переработан в корутину. Сейчас же самым простым путем вникнуть будет сделать свою.
Начнем наше погружение и напишем небольшую асинхронную программу. Это будет классическое "Hello, world!" приложение для асинхронного кода:
Когда вы выполните этот скрипт, обратите внимание, что в нем отличается от стандартного определения через def и time.sleep():
Порядок выходных данных - это главное при асинхронном подходе. Каждая из count() функций запускается в едином цикле событий или координаторе. Когда функция получает await asyncio.sleep(1), функция отдает результат в цикл и отдает контроль над ресурсами, объявляя "сейчас я буду бездействовать следующую секунду, позволь чему-то полезному выполняться в это время"
Сравним с синхронной версией:
Когда код выполнен, мы видим небольшие но критичные изменения в порядке и времени выполнения:
Несмотря на то, что использование time.sleep() и asyncio.sleep() может показаться банальным, эти функции очень часто используются для имитации любых ресурсоемких процессов, которые включают время ожидания.(во время time.sleep() программа только ожидает выполнение процесса и не делает ничего кроме) Итак, time.sleep() может имитировать какой-то требующий большого количества времени процесс, тогда как asyncio.sleep() используется чтобы ожидать выполнение процесса не блокируя основной цикл.
Как вы увидите в следующей части, к преимуществам ожидания с использованием asyncio.sleep() относится то, что остальная часть скрипта на время получает контроль и может отдать его функциям, готовым действовать прямо сейчас. В этом главное отличие от time.sleep() и любой другой блокирующей функции, которые несовместимы с асинхронным кодом в Python, поскольку блокируют все, на время сна-ожидания.
Правила работы с асинхронным кодом
Теперь перейдем к более формальному определению асинхронности, ожидания и функций-корутин. Эта секция написана достаточно плотно, но дает понимание об основных правилах действия async/await, так что, если будет необходимо, можете к ней вернуться:
- синтаксис async def называют естественной корутиной или асинхронным генератором. Выражение async with или async for так же валидны, вы убедитесь в этом позднее.
- Ключевое слово await позволяет функции отдать контроль назад в главный цикл. (который содержит порядок исполнения всех корутин). Если Python ожидает (await) функцию f() из области g(), это выглядит примерно так "Отложить исполнение кода g() до тех пор, пока я не получу результат выполнения f(). В это время я займусь чем-нибудь другим"
В коде второй пункт будет выглядеть примерно так:
Есть определенный список правил, касающийся использования команд async/await. Неважно, вы только начинаете изучать синтаксис или уже начинаете писать асинхронный код, они будут вам полезны:
- Функция, которая начинается с async def - это корутина. Она может использовать await, return или yield, но это опционально. Запись вида: asycn def noop(): pass тоже валидна
- Малораспространенный способ (да и легален он только в Python) - использовать yield в асинхронном блоке функции. Это создает асинхронный генератор, через который вы можете итерироваться с помощью async for. Забудьте пока об асинхронных генераторах и сфокусируйтесь на правильном использовании синтаксиса при создании корутин.
- Все определенное с помощью async def не может использовать yield from. Это вызовет SyntaxError
- Вы можете использовать await только в теле корутины. Вызов этой команды вне корутины спровоцирует SynatxError, как и вызов yield вне
def функции.
Несколько примеров, чтобы суммировать эти правила:
Итак, когда вы используете await, требуется, чтобы объект был awaitable. Звучит не очень понятно,не так ли? На данный момент важно знать, что это может быть (1) корутина или (2) объект у которого определен метод .__await__(), возвращающий итератор. В подавляющем большинстве случаев, вам придется беспокоиться только о случае №1
Это подводит нас к одному техническому решению, которое вы уже могли видеть. Раньше асинхронные функции определялись с помощью декоратора @asyncio.coroutine. Результатом была корутина основанная на генераторах (generator-based coroutine). Эта конструкция была заменена на async/await с Python 3.5
Эти две корутины эквивалентны, но одна из них основана на генераторах, а вторая натуральная корутина:
Если вы пишете код сами, то лучше использовать натуральные корутины, потому что поддержка основанных на генераторах будет убрана из Python, начиная с версии 3.10.
В более поздних частях туториала мы рассмотрим особенности корутин основанных на генераторах, ради широты кругозора. Async/await были введены, чтобы сделать корутины отдельной самостоятельной особенностью Python, которая была бы легкоотличима от обычной функции генератора. Это сделано, чтобы снизить неоднозначность
Не советуем сильно углубляться в изучение корутин на генераторах, которые все равно скоро будут заменены async/await. У них есть небольшой свод собственных правил (в частности, await нельзя использовать в корутине, основанной на генераторах), но он не важен, если вы придерживаетесь правильного синтаксиса async/await.
Без откладывания в долгий ящик, давайте разберем несколько примеров.
Вот пример, как асинхронный подход позволяет сократить время ожидания: дана корутина makerandom(), которая создает случайные целочисленные цифры в диапазоне [0,10], до тех пор, пока одна из них не преодолеет порог. Вы можете вызвать эту корутину несколько раз, нет необходимости ожидать пока другие завершатся успехом. Немного изменим паттерн, который уже был представлен выше:
Цветной выход может сказать вам гораздо больше, чем я и даст понимание того, как работает скрипт:
Программа использует главную корутину makerandom() и запускает её 3 раза одновременно. Большая часть программ обычно содержит маленькие модульные корутины и одну функцию-обертку, которая связывает их вместе. main() используется чтобы собрать задачи через сопоставление с центральной корутиной-итератором или пулом.
В этом небольшом примере пул - это range(3). В следующих частях будут более полные примеры - это будет список URL адресов, по которым необходимо отправить запросы, спарсить и обработать одновременно, и main() инкапсулирует всю эту подпрограмму для каждого URL.
Хоть процесс "создания случайных цифр" (который однозначно полностью зависит от CPU) может быть не самым лучшим примером для иллюстрации задач, решаемых асинхронным подходом, в нем есть asyncio.sleep(), имитирующий процесс обмена данными, при котором время ожидания заранее не определено. Например, asyncio.sleep() может представлять процесс получения-отправки "не случайных цифр" между двумя пользователями в почтовом приложении.
Основные паттерны написания асинхронного кода
Эта секция расскажет о возможных шаблонах, которые часто используются при решении задач получения-отправки данных.
Сцепка корутин
Одна из главных особенностей корутин - это возможность их сцепки. (помните, что результат от корутины можно ждать, а значит другая корутина может получить его через await). Это позволяет разбивать программу на маленькие, управляемые корутины:
Обратите внимание на результат. Первая часть программы работает с различными перерывами, а вторая начинает работать, когда получает данные:
В этом случае общее время работы main() будет равно максимальному времени выполнения функций, которые он собирает.
Использование очередей
Библиотека asyncio так же дает доступ к дополнительным очередям, которые разработаны, чтобы быть похожими на стандартные очереди. В наших примерах еще не было необходимости использовать подобные структуры. В прошлом примере каждая задача разобрана на множество корутин, которые ожидают друг друга. На каждую такую цепь есть лишь один вход.
Есть альтернативная структура, которая так же может работать с асинхронным кодом: определенное количество независимых объектов-производителей может создавать элементы и помещать их в очередь. Каждый такой производитель может добавлять множество элементов случайное количество раз. Группа объектов-потребителей будет тянуть элементы из очереди как только они будут появляться без ожидания каких-либо сигналов.
При таком подходе, нет связанных цепочек производитель-потребитель. Потребитель не знает номера производителя или даже общее количество элементов в очереди.
Для каждого производителя и потребителя необходимо разное количество времени на связь с очередью. Очередь служит пропускным пунктом, которые связывает потребителей и производителей без "личных контактов".
Заметка: при построении многопоточных конструкций, часто используется очереди, так как они относительно безопасны (queue.Queue()). При работе с асинхронным кодом вы не обязаны дополнительно страховаться и использовать "безопасные очереди" (исключение, если вы комбинируете оба этих подхода, но такой случай выходит за рамки этой статьи)
Одним из примеров для очередей - использовать как связку между производителями и потребителями, которые иначе никак не связаны между собой.
Синхронная версия такой программы будет выглядеть удурчающе: группа блокирующих процессов добавляет элементы в очередь, по одному производителю за раз. Только после того как все производители закончат, очередь начнет разгружаться потребителями, по одному элементу за раз. Результатом такого подхода будет куча задержек. Элементы будут долгое время находиться в очереди, вместо того чтобы быть разгруженными по немедленно.
Асинхронная версия представлена ниже. Самая сложная часть - организация общего потока и сигнала о завершении цикла производства. В противном случае await q.get() будет постоянно зависать, так как очередь будет обработана, но потребители не будут знать, что элементов больше не поступит.
(большое спасибо за помощь пользователю со StackOverflow с исправлением main(): ключ в том, чтобы ждать q.join(), который блокируется до тех пор, пока все элементы в очереди не будут обработаны и затем отменяет задачи потребителей. Иначе потребители бы бесконечно проверяли очередь, ожидая появления новых элементов.)
Сам скрипт:
Первые несколько корутин - вспомогательные. Они возвращают случайную строку, счетчик производительности с долями секунд и число. Каждый производитель может поместить в очередь от 1 до 5 элементов. Каждый элемент представляет из себя кортеж (i, t) где i - это случайная строка, а t - время, в которое производитель добавил строку в очередь.
Когда потребитель вытягивает элемент, он просто подсчитывает время, которое элемент провел в очереди.
asyncio.sleep() используется, чтобы смоделировать другие, более сложные корутины, которые могли бы занять время и заблокировать выполнение, как если бы они были обычными блокирующими функциями.
Тестовый запуск, с 2 производителями и 5 потребителями:
В этом случае создание и обработка элементов занимает доли секунды. Задержка может быть вызвана 2 причинами:
- Стандартные, чаще всего неконтролируемые накладные расходы
- Ситуация, когда все потребители спят, а элементы появляются в очереди.
Что касается второй причины, совершенно нормально увеличивать количество потребителей до сотен и тысяч. Не должно быть проблем при python3 asyncq.py -p 5 -c 100. (5 производителей, 100 потребителей) Суть в том, что теоретически вы можете иметь разных пользователей на разных машинах и системах, которые контролируют потребителей и производителей. Тогда очередь служит центральным "пропускным пунктом".
Вот так, с места в карьер, но мы рассмотрели три основных примера асинхронного кода: корутин, определенных с помощью await/async. Если вы хотите поглубже разобраться в этом вопросе, мы с вами начнем серьезно разбиваться в следующей части.
__________________________________________________________________________________________
Спасибо за чтение) Материал получился объемным, оставшееся тянет ещё на 2-3 части. Оставайтесь с нами)