Упрощаем разработку асинхронные функции Python


Оглавление (нажмите, чтобы открыть):

BreakingCode

Асинхронное выполнение кода в Python и Django

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

Разные варианты, описанные ниже, подходят для разных случаев. Например, использование threading не всегда допустимо при работе с Django, в то время как в небольших скриптах нецелесообразно использование связки celery и redis.

Зачем нужен асинхронный подход?

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

Или пример прямо из жизни. Пользователь загружает объемный файл через веб-интерфейс (используется Django), обработка которого на стороне сервера может занять до 3 минут. Естественно, что во view, которая обрабатывает запрос пользователя, обрабатывать файл нельзя — это заблокирует весь поток. В таких случаях и помогает асинхронный подход.

Асинхронный код с помощью threading

Этот вариант — самый простой, и, вероятно, не всегда применимый и правильный. Заключается он в использовании модуля threading.

Код ниже вызывает функцию test_long асинхронно.

В частности, такой подход можно использовать и в Django. Он будет работать, если поток асинхронной функции не использует объекты, уничтожаемые Django после отправки ответа (response).

Асинхронное программирование с Tornado

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

Давайте посмотрим на примеры, взятые отсюда:

Не смотря на корректный вызов my_function, все равно возникнет блокировка. Это результат использования time.sleep(1), который заблокирует весь сервер. Правильный вариант будет выглядеть так:

По использованию с Django есть примеры.

Использование celery в Django

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

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

Отличный мануал по подключению есть у Алексея здесь. Установка и настройка по мануалу труда не составляет.

Регулирование асинхронных функций в Python Asyncio

У меня есть list из awaitables который я хочу передать asyncio.AbstractEventLoop , но мне нужно регулировать запросы к стороннему API.

Я хотел бы избежать чего-то, что ожидает передачи future в цикл, потому что в то же время я блокирую ожидание цикла. Какие варианты у меня есть? Semaphores и ThreadPools ограничат количество одновременно работающих, но это не моя проблема. Мне нужно увеличить количество запросов до 100 в секунду, но не имеет значения, сколько времени потребуется для выполнения запроса.

Это очень лаконичный (не) рабочий пример использования стандартной библиотеки, который демонстрирует проблему. Предполагается, что это дросселирует со скоростью 100 /сек, но дросселирует при 116,651 /сек Как лучше всего управлять расписанием асинхронного запроса в asyncio ?

Изменить. Я добавил простой пример TrottleTestApp , используя семафоры, но все еще не могу ограничить выполнение:

1 ответ

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

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

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

или вы можете позвонить acquire() напрямую:

или вы можете просто проверить, есть ли место сначала:

Обратите внимание, что некоторые запросы можно считать «более тяжелыми» или «более легкими», увеличивая или уменьшая количество, которое вы «капаете» в корзину:

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

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

Максимальный размер пакета равен максимальному значению скорости в приведенной выше демонстрации, которая была установлена ​​на 5. Если вы не хотите разрешать пакеты, установите максимальную скорость на 1, а период времени равным минимальному времени между капает:

Новый Python: Асинхронное всё

отступление 1: Сопрограммы (coroutines)

Сопрограмма — это такая сущность, которой можно передавать и получать назад управление и данные и которая (в отличие от процедуры) хранит внутреннее состояние. Можно заметить, что классические генераторы это уже почти сопрограммы, но не совсем:

  • generators cannot yield control while other functions are executing, unless those functions are themselves expressed as generators,
  • генератор только отдает значения, но необходимо еще иметь возможность засовывать данные внутрь.

И так действительно можно:

еще можно закидывать исключения в итератор: it.exception()

в результате получаем полноценную сопрограмму: можем передавать и управление, и значения в обе стороны.

отступление 2: yield from

но (см. выше) yield же может принимать значения снаружи поэтому yield from

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

когда выполнение натыкается на yield from subg() , все приходящие через send()/next() запросы проксируются в subg , а yield ‘имые значения соответственно передаются сразу наружу. Так происходит до тех пор, пока subg не кончится (т.е. не выбросит StopIteration), после этого выполнение внешнего генератора продолжается.

пример (вкуривать пока не станет понятно):

а работает он вот так:

asyncio

с чего всё началось: транспорт и протокол

  • протокол осуществляет доставку байтов,
  • транспорт предоставляет/получает эти байты;
  • они общаются подергиванием неблокирующих методов друг друга

event loop


Такой объект со специфицированным интерфейсом, который выполняет функции планировщика. Собственно, в него передаётся управление, когда асинхронная сопрограмма залипает на блокирующей операции.

основные методы:

запуск и остановка: * run_forever() * run_until_complete(future) (про futures ниже) * stop() : stops the event loop as soon as it is convenient , потом можно рестартить * close() : освобождает все занятые ресурсы This should not be called while the event loop is running

  • call_soon(callback, *args) . This schedules a callback to be called as soon as possible. Returns a Handle (see below) representing the callback, whose cancel() method can be used to cancel the callback. Сохраняет порядок выполнения.
  • call_later(delay, callback, *args) через n секунд, НЕ сохраняет порядок
  • call_at(when, callback, *args) через это реализуется call_later
  • time() чтобы знать, что отдавать call_at

event loop либо глобален (и добывается через event loop policy , такой специальный объект), либо явным образом передается во все функции, которые с ним работают (грепать в пепе: event loop policy; Passing an Event Loop Around Explicitly)

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

asyncio предоставляет две реализации: SelectorEventLoop и ProactorEventLoop, последний работает только на винде и около.

Хэндлы нужны, чтоб отменить задание через cancel() . Других применений и публичных методов у них нет.

отступление 3: Futures

изначально идея зародилась в concurrent (многотредовость), а потом идею зохавал asyncio. Собственно, в concurrent это выглядит следующим образом: Есть два базовых класса: Executor и Future Executor’у даются заявки на выполнение, он в ответ возвращает Future, который является представителем задания и в который можно тыкать и спрашивать, как оно поживает.

Futures в asyncio работают примерно так же.

Основное: Future можно использовать для отмены задачи, как хэндл, и можно ему отдавать коллбэки, которые он дёрнет по завершении задачи. Если Future на момент прикручивания коллбэка уже выполнен, то последний дёргается через call_soon() Каждый такой объект при создании ассоциируется с эвентлупом, который занимается заданием и дергает за все эти ниточки. * asyncio’шный Future можн использовать в правой стороне yield from (т.е. yield from footure ), что имеет эффект «дождись окончания исполнения и отдай результат наружу».

есть волшебные методы:

asyncio.async(arg) : делает Future из аргумента (в качестве аргумента допустимо всё что можно писать справа от yield from , т.е. сопрограмма либо future)

asyncio.wrap_future(future) : обертка-адаптер для concurrent.futures.Future , чтоб оно работало с asyncio

Это всё было про коллбэки.

Теперь интересное: можно коллбэками не пользоваться, а пользоваться сопрограммами.

Things a coroutine can do: result = yield from future — suspends the coroutine until the future is done, then returns the future’s result result = yield from coroutine() — wait for another coroutine to produce a result return expression raise exception

pep 492: async/await syntax

понятие native coroutine (т.е. не из генератора)

  • It is a SyntaxError to have yield or yield from expressions in an async function теперь можно (см. следующую главу)
  • Regular generators, when called, return a generator object ; similarly, coroutines return a coroutine object
  • StopIteration exceptions are not propagated out of coroutines, and are replaced with a RuntimeError . For regular generators such behavior requires a future import (see PEP 479).

await

ждем ответа от чего-либо, тем временем передаём управление в event loop

с точки зрения данной сопрограммы это просто блочит выполнение, пока awaitable не завершится

т.е. db.fetch() возвращает специальный awaitable объект, от которого далее можно ждать ответа

в случае asyncio awaitable это Future собственно, awaitable бывают трех видов: * A native ( async def ) coroutine object
* A generator-based coroutine object (NB: генератор надо декорировать при помощи types.coroutine() ) * An object with an __await__ method returning an iterator. (такое называется Future-like objects )

когда мы говорим await foo , мы отдаем управление эвентлупу до тех пор пока foo не закончит (+эпсилон)

async with

в случае с обычным with контекст глобальный. Когда мы можем передавать управление в сильно другие места (где может быть другой with это приводит к волшебным багам, поэтому нужен специальный asynchronous context manager, который умеет обрабатывать такие переключения

(Two new magic methods are added: __aenter__ and __aexit__ ).

async for

работает только внутри нативной сопрограммы ( async def ) нужен специальный asynchronous iterable __aiter__ -> __anext__ -> StopAsyncIteration

собственно async for:

A new statement for iterating through asynchronous iterators is proposed:

which is semantically equivalent to:

asynchronous comprehensions

Конечно же, для асинхронного for можно использовать короткий синтаксис генераторов:

  • set comprehension: ;
  • list comprehension: [i async for i in agen()] ;
  • dict comprehension: ;
  • generator expression: (i ** 2 async for i in agen()) .

еще теперь можно в них использовать await:

This is only valid in async def function body.

асинхронные генераторы

мы умеем легко писать генераторы:

несмотря на то, что на самом деле интерфейс генератора упорот и магическ: __iter__ , __next__ , вот это всё.

Ломающие новости! теперь асинхронные генераторы тоже можно так писать, не реализуя руками __aiter__() и остальные кишки.

(а еще они работают в 2 раза быстрее «ручной» реализации)

доп.чтение: PEP 533

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

H Как же, черт побери, работает async/await в Python 3.5 в черновиках

.collapse»>Содержание

От переводчика: Это перевод статьи
https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/.
Оригинальная статья показалась мне очень полезной и, как мне кажется, определенно заслуживает внимания, если вы до сих пор плохо представляете, как работает асинхронное программирование в Python.
Все ссылки на сторонние ресурсы, встречающиеся в оригинальном тексте, сохранены как есть. Очень советую прочитать информацию по этим ссылка, в особенности различные PEP, тогда многое встанет на свои места.
Перевод в некоторых местах является достаточно вольным, многие выражения переведены не дословно, но с сохранением основного смысла. Все же русский и английский — разные языки и дословный перевод не всегда лучше и понятнее.
Некоторые термины имеют оригинальное написание рядом в скобках. Это сделано с целью сохранения изначального смысла и для возможности сопоставления с оригинальными техническими терминами в документации на английском языке.
У меня получилось перевести не все термины, некоторые можно перевести по-разному. Если вы знаете, как точно переводится тот или иной термин, прошу указывать это в комментариях. Если такой перевод сделает смысл более понятным, я с удовольствием его добавлю.
Если после прочтения у вас останутся вопросы или вы заметили неточность, обязательно напишите об этом в комментариях.
Приятного чтения!

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

Но до недавнего момента я не представлял, как async / await работает в Python 3.5. Я знал, что yield from в Python 3.3 в купе с asyncio в Python 3.4 привел к появлению этого нового синтаксиса. Но тот факт, что мне не часто приходилось писать код, работающий с сетью (на чем asyncio как раз и фокусируется, но не ограничивается), вылился в итоге в то, что я мало внимания уделял теме async / await . Я конечно знал, что

в целом эквивалентен конструкции

Я также знал, что asyncio — это event loop фреймворк, который используется для асинхронного программирования, и что в реальности означают эти слова. Но, никогда особо не погружаясь в синтаксис async / await для понимания, как это все работает вместе, я чувствовал, что не понимаю, как устроено асинхронное программирование в Python, и это меня беспокоило. Поэтому я решил все же найти время и разобраться, как же, черт побери, это все устроено. И, поскольку я слышал от различных людей, что они тоже не понимают, как этот новый мир асинхронного программирования функционирует, я решил написать это эссе (да, этот пост занял так много времени и содержит так много слов, что моя жена в итоге окрестила его эссе).


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

Исторический урок о сопрограммах в Python

Согласно Википедии, «Сопрограммы (coroutines) (прим. переводчика: вот ссылка на определение этого термина в русской версии Википедии, далее идет выжимка именно оттуда) — методика связи программных модулей друг с другом по принципу кооперативной многозадачности: модуль приостанавливается в определённой точке, сохраняя полное состояние (включая стек вызовов и счётчик команд), и передаёт управление другому. Тот, в свою очередь, выполняет задачу и передаёт управление обратно, сохраняя свои стек и счётчик». Это упрощенно можно перефразировать так «сопрограммы — это функции, выполнение которых вы можете приостанавливать». И если вы скажете «дак это то, как работают генераторы», вы будете абсолютно правы.

Еще в Python 2.2 генераторы впервые были представлены в PEP 255 (они также назывались генераторами-итераторами поскольку реализовывали интерфейс итератора). Первоначально вдохновленные на создание языком программирования Icon, генераторы позволяли создать итератор в наиболее простой форме, который не расходовал бы память при вычислении следующего значения в процессе итерации по нему (вы конечно же могли бы создать целый класс, который реализует функции __iter__() и __next__() и не хранит каждое значение итератора, но на это потребуется больше времени). Например, если бы вы захотели создать свой вариант range() , то вы могли бы реализовать его вот таким очень затратным по памяти способом, создав просто список с целыми числами:

Проблема в том, что несмотря на все же реализованную последовательность целых чисел от 0 до 1,000,000, вам пришлось создать в памяти очень большой список из 1,000,000 чисел. Но после того, как в язык были добавлены генераторы, у вас появилась возможность с минимальными затратами создать итератор, которому не нужно генерировать всю последовательность за раз. Вместо этого достаточно иметь память для хранения только одного числа в каждый момент времени.

Возможность останавливать выполнение функции в любой момент времени, когда в коде встречается выражение yield (до версии Python 2.5 эта конструкция была оператором), и позже возобновлять выполнение функции с этого места оказалась очень полезной в контексте более эффективного использования памяти, позволяя реализовать концепцию бесконечных последовательностей и другие похожие вещи.

Но, как вы уже успели заметить, генераторы — это все же итераторы. Иметь возможность простого создания итераторов — это конечно замечательно (и вполне очевидно, когда достаточно просто определить метод __iter__() в объекте, который используется в качестве генератора), но люди знали, что если взять из генераторов механизм «постановки на паузу» и добавить к нему возможность «отправки чего-либо обратно» в генератор, внезапно Python получил бы возможность использовать идею сопрограмм (но пока я не сказал обратное, считайте, что это всего лишь идея в Python, сами сопрограммы обсуждаются далее по тексту). И такая возможность отправки чего-либо обратно в остановленный генератор была добавлена в Python 2.5 благодаря PEP 342. Помимо других вещей, PEP 342 добавил в генераторы метод send() . Это уже позволило не только останавливать генераторы, но и отправлять значения обратно в генератор в место останова. Взяв наш пример выше с функцией range() , вы можете добавить возможность перемещения по последовательности вперед и назад на некоторое количество шагов:

Генераторы больше не трогали до Python 3.3, в котором в рамках PEP 380 добавили выражение yield from . Строго говоря, это выражение дает возможность выполнить более чистый рефакторинг генераторов, позволяя одной простой конструкцией выдавать каждое значение из итератора (которым обычно оказывается генератор):

Сделав рефакторинг более простым, yield from также позволил создавать цепочку из генераторов таким образом, что значения могли всплывать и опускаться обратно по стэку вызовов без использования всякого дополнительного кода.

Резюме

Генераторы, добавленные в Python 2.2, позволили останавливать выполнение кода. С появлением функционала отправки значения обратно в остановленный генератор, который внедрили в Python 2.5, в нем появилась возможность реализации идеи сопрограмм. А добавление yield from в Python 3.3 не только упростило рефакторинг генераторов, но и позволило выстраивать их в цепочки.

Что такое event loop?

Если вы хотите разобраться с async / await , важно понимать, что такое event loop, и как он позволяет сделать асинхронное программирование возможным. Если вы ранее участвовали в создании графического интерфейса пользователя (GUI) (сюда входит и клиентская вэб-разработка), тогда у вас уже должен быть опыт работы с event loop. Но, поскольку идея асинхронного программирования в Python, выраженная в этой языковой конструкции, появилась достаточно недавно, это вполне нормально, что вы не знаете, что такое event loop.

Обращаясь к Википедии, event loop — это «программная конструкция, которая ожидает и генерирует события или сообщения в программе». По сути event loop позволяет реализовать логику «когда случилось A, выполнить В». Возможно, наиболее простым примером этой идеи является event loop в JavaScript, который присутствует в любом браузере. В момент, когда вы нажали что-то на странице («когда случилось А»), событие нажатия попадает в event loop, который в свою очередь проверяет, была ли ранее зарегистрирована функция обратного вызова (callback) на событие onclick для того, чтобы обработать это нажатие («выполнить В»). Если какой-либо обработчик был зарегистрирован для этого события, он вызывается и получает объект с информацией о событии. Event loop (прим. переводчика: этот термин можно дословно перевести, как «петля событий») называется так, потому что он постоянно собирает события и, перебирая их, пытается выяснить, как обработать каждое из этих событий.

В случае с Python, asyncio был добавлен в стандартную библиотеку языка, чтобы обеспечить как раз реализацию идеи event loop. В asyncio основной фокус сделан на сеть, что в случае с event loop подразумевает, что частным случаем «когда случилось А» может являться событие готовности сокета к чтению и/или записи (I/O) (посредством модуля selectors ). Помимо графического интерфейса пользователя (GUI) и сетевого ввода/вывода (I/O), event loop также часто используется для организации исполнения кода в отдельном потоке или дочернем процессе и выступает в качестве планировщика (кооперативная многозадачность). Если вы понимаете, как работает GIL в Python, event loop’ы могут быть очень кстати в ситуациях, когда отключение GIL в принципе возможно и это будет даже эффективно.

Резюме

Event loop позволяет организовать логику «когда произошло А, сделай В». Проше говоря, event loop наблюдает за тем, не произошло ли «что-то», за что он отвечает, и если это «что-то» случилось, он вызывает код, который должен обработать это событие. Python включил event loop в стандартную библиотеку в виде asyncio начиная с версии Python 3.4.

Как работают async и await

Как это было реализовано в Python 3.4

С нововведениями в работе генераторов, добавленных в Python 3.3, и появлением event loop в виде asyncio Python 3.4 уже имел достаточно для поддержки асинхронного программирования в форме concurrent программирования (прим. переводчика. Термин «concurrent» условно можно перевести как «одновременный». Но «одновременное программирование» звучит странно и имеет совершенно другой смысл. Если кто-то подскажет, как лучше это перевести, буду очень признателен). Асинхронное программирование — это такое программирование, когда порядок выполнения неизвестен заранее (поскольку выполнение асинхронно, а не синхронно). Concurrent программирование — это написание такого кода, который может выполняться независимо от других частей, даже если они все выполняются в одном потоке (одновременное выполнение — это не параллелизм). К примеру, следующий код на Python 3.4 реализует таймер обратного отсчета с помощью двух асинхронных, одновременных вызовов одной и той же функции.

В Python 3.4 декоратор asyncio.coroutine использовался для того, чтобы пометить функцию, которая является сопрограммой, и написана для использования с пакетом asyncio и его реализацией event loop. Это обозначило первое определение сопрограмм в Python: это такой объект, который реализует методы, добавленные генераторам в рамках PEP 342, и предоставляемые абстрактным базовым классом collections.abc.Coroutine. А это в свою очередь означало, что все генераторы внезапно получили интерфейс сопрограмм, даже несмотря на то, что изначально их даже не планировалось использовать в таком ключе. Чтобы это исправить, asyncio потребовал, чтобы все генераторы, являющиеся сопрограммами, были обернуты в декоратор asyncio.coroutine.

С таким однозначным обозначением сопрограммы (которое совпадало с интерфейсом генераторов) вы затем просто использовали yield from с любым объектом asyncio.Future , чтобы отправить его в event loop, останавливая выполнение сопрограммы до возникновения какого-либо события (реализация future-объекта описана в asyncio и не особо важна в данном контексте). Когда future-объект попадал в event loop, он начинал проверяться на предмет завершения той задачи, за выполнение которой он отвечал. Как только его задача завершалась, event loop оповещался о завершении и остановленная ранее и ожидающая этот future-объект сопрограмма запускалась вновь с точки останова и посредством метода send() в нее передавался результат выполнения future-объекта.

Возьмем пример, приведенный выше. Event loop запускает каждую сопрограмму countdown() , выполняя код до выражения yield from и функции asyncio.sleep() в одной из сопрограмм. Это выражение возвращает объект asyncio.Future , который отправляется в event loop и останавливает выполнение сопрограммы. Далее event loop наблюдает за этим future-объектом до его завершения, по факту пока не истечет секунда (также проверяя и другие объекты, такие как вторая сопрограмма). Как только секунда истекла, event loop берет остановленную сопрограмму countdown() , которая ранее вернула future-объект, и отправляет результат завершения future-объекта в сопрограмму в то самое место, откуда она вернула future-объект, а затем сопрограмма вновь запускается. Так продолжается до тех пор, пока все запущенные сопрограммы countdown() не завершатся и внутри event loop не останется объектов для наблюдения. Позже мы рассмотрим более полный пример того, как сопрограммы и event loop работают в связке, но для начала я расскажу, как работает async и await .

Переход от yield from к await в Python 3.5

Функция, которая помечалась как сопрограмма для использования в асинхронном программировании, в Python 3.4 выглядела так:

В Python 3.5 был добавлен декоратор types.coroutine , который помечает генератор как сопрограмму аналогично asyncio.coroutine . Также можно использовать конструкцию async def , чтобы синтаксически обозначить функцию, являющуюся сопрограммой, несмотря на то, что она не может содержать выражения yield , допускается использовать только return и await для возврата значения из такой сопрограммы.

По сути async и types.coroutine сужают определение того, что считается сопрограммой. Это меняет определение сопрограмм от «просто интерфейс генератора», сделав различие между любым генератором и генератором, используемым как сопрограмма, намного более строгим (даже функция inspect.iscoroutine() теперь выполняет более строгую проверку, сообщая, что для сопрограммы необходимо использовать async ).

Вы наверняка уже заметили, что в примере, написанном для Python 3.5, помимо async также появилось выражение await (которое, кстати, можно использовать только вместе с async def ). Несмотря на то, что await работает аналогично yield from , объекты, которые может принимать выражение await , другие. Для использования в выражении await конечно же разрешены сопрограммы, поскольку весь фундамент концептуально строится на базе сопрограмм. Но когда вы передаете в await объект, с технической точки зрения достаточно, чтобы этот объект был объектом типа awaitable — это такой объект, в котором определен метод __await__() , возвращающий итератор, который в свою очередь не является сопрограммой. Сами сопрограммы также считаются awaitable объектами (вот почему collections.abc.Coroutine наследуется от collections.abc.Awaitable ). Такой подход следует уже известной традиции языка Python, когда большинство синтаксических конструкций «под капотом» транслируются в вызовы методов, например, a + b это по сути вызов функции a.__add__(b) или b.__radd(a) .

Как же в итоге обыгрывается различие между yield from и await на более низком уровне (то есть, разница между генератором с декоратором types.coroutine и определенным с помощью выражения async def )? Чтобы это выяснить, давайте взглянем на байткод двух примеров, представленных выше. Байткод для функции py34_coro() выглядит так:

А байткод для функции py35_coro() такой:

Игнорируя разницу в номерах строк, которая может возникнуть из-за того, что py34_coro() обернут в декоратор, можно увидеть одно единственное отличие — это код операции GET_YIELD_FROM_ITER в одном случае и GET_AWAITABLE в другом. Обе функции помечены флагом, который определяет, что функции являются сопрограммами, так что в этой части различий нет. В случае же с GET_YIELD_FROM_ITER , декоратор проверяет, является его аргумент генератором или сопрограммой, и, если это не так, он оборачивает аргумент в функцию iter() (выражение yield from принимает сопроцедуру только тогда, когда на ней установлен соответствующий код операции, что по сути и делает декоратор types.coroutine , проставляя флаг CO_ITERABLE_COROUTINE объекту кода генератора на уровне языка C).

Но GET_AWAITABLE байткод обрабатывает по-другому. В то время как байткод принимает сопрограмму, как это происходит и в случае с GET_YIELD_FROM_ITER , он не примет генератор, который не помечен как сопрограмма. Также помимо обычных сопрограмм байткод принимает awaitable объект, рассмотренный ранее. Из этого следует, что оба выражения yield from и await принимают сопрограммы, но различаются тем, что первый также может принимать простые генераторы, а второй — объекты типа awaitable.

Вам наверняка интересно, зачем введено такое ограничение между типами объектов, которые может принимать async -сопрограмма и сопрограмма на базе генератора в их выражении останова сопрограммы? Основная цель такого подхода в том, чтобы не дать вам случайно перепутать объекты, которые в лучших традициях Python просто могут иметь общий API. Поскольку генераторы по своей сути реализуют API сопрограмм, можно очень легко и совершенно случайно попытаться использовать генератор вместо предполагаемой сопрограммы. А поскольку не все генераторы написаны таким образом, что их можно использовать в качестве сопрограмм, необходимо полностью исключить такое неправильное использование генератора. И, поскольку Python не является статически компилируемым, лучшее, что может предложить язык в такой ситуации — это проверки на соответствие во время выполнения кода. Это значит, что даже если используется types.coroutine , компилятор не в состоянии определить, как будет использоваться генератор: как сопрограмма или как обычный генератор (помните, что даже если синтаксически имеется функция с декоратором types.coroutine , это не гарантирует, что кто-то ранее не сделал замену types = spam ). Поэтому различные коды операции, которые имеют отличные друг от друга ограничения, генерируются компилятором на базе той информации, которая у него есть в момент выполнения (прим. переводчика. Автор статьи хотел пояснить, что коды операции используются для однозначного разделения сопрограмм на базе генераторов, которые были до появления awaitable объектов, от родных сопрограмм, которые используют только awaitable объекты. А при одинаковых интерфейсах иначе, кроме как выставлением кода операции на самом объекте сопроцедуры, эту задачу не решить).

Хотелось бы сделать одно существенное замечание касательно разницы между сопрограммами на базе генераторов и сопрограммами с использованием async . Только сопрограммы на базе генераторов могут действительно останавливать выполнение и отсылать что-либо в event loop. Как правило, вы не замечаете эту важную деталь, потому что обычно используете функции, которые существуют в экосистеме event loop, такие как asyncio.sleep() , а поскольку event loop’ы реализуют свой собственный API, то о реализации этой важной детали заботятся сами функции из этой экосистемы. Большинство из нас чаще использует event loop нежели реализует его логику, поэтому, используя только сопрограммы на базе async , вам по большому счету будет все равно, как они работают внутри. Но если вам ранее было интересно, почему вы не могли создать что-то похожее на asyncio.sleep() , используя только сопрограммы на базе async , то это как раз тот самый момент истины.

Резюме

Давайте подведем итог. Определение метода с использование async def превращает его в сопрограмму. Другой способ определить сопрограмму — это пометить генератор с помощью types.coroutine (с технической точки зрения, она выставит флаг CO_ITERABLE_COROUTINE в качестве кода операции на объекте генератора) или сделать его подклассом collections.abc.Coroutine . Остановить цикл выполнения сопрограммы можно только с помощью сопрограммы на базе генератора.

Объект awaitable — это либо сопрограмма, либо объект, реализующий метод __await__ (с технической точки зрения, это по сути интерфейс абстрактного класса collections.abc.Awaitable ), который возвращает итератор, не являющийся сопрограммой. Выражение await по сути является выражением yield from , но с ограничением использования только с объектами awaitable (обычные генераторы не будут работать с выражением await ). Функция async — это сопрограмма, в которой имеются операторы return (сюда также входит неявный return None , который по умолчанию присутствует в конце каждой функции в Python) и/или выражения await (использование выражений yield не допускается). Ограничения для async функций введены для того, чтобы вы по ошибке не перепутали сопрограммы на базе генераторов с другими генераторами, поскольку ожидаемое использование обоих типов генераторов в корне отличается.

Думайте об async / await как о программном интерфейсе для асинхронного программирования

Я хочу отметить одну ключевую особенность, о которой я глубоко не задумывался, пока не посмотрел доклад Дэвида Бизли на Python Brazil 2015. В этом докладе Дэвид сделал акцент на том, что async / await на самом деле является программным интерфейсом для асинхронного программирования (о чем он упомянул еще раз в Twitter.) Дэвид считает, что люди не дожны думать об async / await как о синониме asyncio , а вместо этого расценивать asyncio как отдельный фреймворк, который может использовать программный интерфейс async / await для реализации асинхронного программирования.

На самом деле Дэвид настолько верит в идею того, что async / await — это всего лишь программный интерфейс для асинхронного программирования, что даже создал проект curio , в котором реализовал свой собственный event loop. Это помогло мне прояснить тот факт, что async / await представляют в Python всего лишь строительные блоки для реализации асинхронного программирования, но без привязки к конкретной реализации event loop или другим низкоуровневым вещам (в отличие от других языков программирования, в которых event loop непосредственно интегрирован в сам язык). Это позволяет проектам вроде curio не просто работать по-другому на более низком уровне (например, asyncio использует future-объекты как API для взаимодействия со своим event loop, в то время как curio использует для этого кортежи), но и иметь другие цели и иную производительность (например, asyncio включает в себя целый фреймворк для реализации слоев транспорта и протокола, что делает его достаточно расширяемым, в то время как curio сам по себе проще и перекладывает эту задачу на плечи разработчика, что в свою очередь позволяет ему работать быстрее).

Учитывая (короткую) историю асинхронного программирования в Python, становится понятно, почему люди могут считать, что async / await == asyncio . Я имею ввиду, что asyncio — это то, что позволило сделать возможным асинхронное программирование в Python 3.4 и было мотиватором для добавления async / await в Python 3.5. Но идея, заложенная в основе async / await , изначально такова, чтобы не требовать для работы сам asyncio или каким-либо способом искажать архитектурные решения в пользу именно этого фреймворка. Другими словами, async / await продолжает традицию Python в создании вещей, как можно более гибких, но в то же время достаточно практичных в использовании (и реализации).

Пример реализации

После всего прочтенного ваша голова наверняка слегка переполнена новыми терминами и понятиями, затрудняя охват всех нюансов реализации асинхронного программирования. Чтобы помочь вам расставить точки над i, далее приведен всеобъемлющий (хотя и надуманный) пример реализации асинхронного программирования, начиная от event loop и заканчивая функциями, относящимися к пользовательскому коду. Пример содержит сопрограммы, представляющие собой отдельные таймеры обратного отсчета, но выполняющиеся совместно. Это и есть асинхронное программирование через одновременное выполнение, по сути 3 отдельных сопрограммы будут запущены независимо друг от друга и все это будет выполняться в одном единственном потоке.

Как я и сказал, это надуманный пример, но если вы запустите этот код в Python 3.5, вы заметите, что все 3 сопрограммы выполняются независимо друг от друга в одном потоке, а также, что общее время выполнения составляет около 5 секунд. Вы можете расценивать Task , SleepingLoop и sleep() как то, что уже реализовано в таких фреймворках как asyncio и curio . Для обычного же пользователя важен только код в функциях countdown() и main() . Можно заметить, что тут нет никакой магии относительно async , await или всего этого асинхронного программирования. Это всего лишь программный интерфейс, который Python предоставляет вам, как разработчику, чтобы сделать такие вещи более простыми в реализации.

Мои надежды и мечты относительно будущего

Теперь, когда я в полной мере понимаю, как работает асинхронное программирование в Python, у меня появилось желание использовать его постоянно! Это настолько крутая идея, что намного лучше подходит для реализации всего того, для чего вы бы ранее стали использовать отдельные потоки. Основная проблема в том, что Python 3.5 еще относительно свежий, как и async / await . А это значит, что пока существует не так много библиотек для поддержки такого рода асинхронного программирования. Например, чтобы реализовать выполнение HTTP-запросов, вам придется либо самому писать всю реализацию HTTP запроса (какая гадость), либо использовать такой фреймворк, как aiohttp , который добавляет HTTP поверх другого event loop (в нашем случае это asyncio ), либо надеяться, что продолжат появляться такие проекты как библиотека hyper для обеспечения абстракций таких вещей как HTTP, что позволит в конечном счете использовать любую библиотеку ввода/вывода (даже несмотря на то, что на данный момент hyper поддерживает только HTTP/2).

Лично я надеюсь, что такие проекты как hyper будут иметь успех, чтобы мы смогли получить четкое разделение логики получения бинарных данных из канала ввода/вывода и логики интерпретации этих самых данных. Такая абстракция очень важна, поскольку в большинстве библиотек ввода/вывода в Python присутствует достаточно сильная связность между реализацией канала ввода/вывода и обработкой данных, которые пришли из этого канала. Это основная проблема пакета http стандартной библиотеки Python, поскольку в нем отсутствует отдельный HTTP-анализатор, а объект соединения выполняет всю работу по вводу/выводу и обработке. И если вы надеялись, что requests получит поддержку асинхронного программирования, вашим надеждам не суждено сбыться, поскольку синхронный ввод/вывод является неотделимой частью общей архитектуры. Этот сдвиг и появление возможности использовать асинхронное программирование в Python предоставляет шанс сообществу Python исправить ситуацию с отсутствием необходимых абстракций на разных уровнях сетевого стэка. И теперь у нас есть преимущество в виде возможности написания асинхронного кода так, как будто это синхронный код, поэтому инструменты, заполняющие пустоту в области асинхронного программирования, могут использоваться в обоих мирах.

Я также надеюсь, что Python получит необходимую поддержку yield в сопрограммах на базе async . Это может потребовать введения еще одного ключевого слова в синтаксис (возможно, что-то типа anticipate ?), но сам факт того, что сейчас нельзя реализовать систему на базе event loop только с использованием сопрограмм async , меня тревожит. К счастью, я такой не один и, поскольку автор PEP 492 солидарен со мной, есть шанс, что эта странность будет исправлена в будущем.

Заключение

По сути async и await являются модными генераторами, которые мы называем сопрограммами, вдобавок имеется дополнительная поддержка для вещей, называемых awaitable-объектами, и функционал для преобразования простых генераторов в сопрограммы. Все это вместе призвано обеспечить одновременное выполнение, чтобы мы имели наиболее полную поддержку асинхронного программирования в Python. Сопрограммы круче и намного проще в использовании, чем такие сравнимые по функционалу решения, как потоки (я ранее написал пример с использованием асинхронного программирования менее чем из 100 строк с учетом комментариев), в то же время это решение является достаточно гибким и быстрым с точки зрения производительности ( FAQ проекта curio содержит информацию, что curio быстрее, чем twisted на 30-40%, но медленнее gevent всего на 10-15%, и это с учетом того, что он полностью написан на чистом Python, а учитывая тот факт, что Python 2 + Twisted может использовать меньше памяти и проще в отладке чем Go, только представьте, что вы можете сделать с его помощью!). Я очень рад, что это прижилось в Python 3 и я с радостью наблюдаю, как сообщество принимает это и начинает помогать с поддержкой в библиотеках и фреймворках, чтобы мы все в итоге только выиграли от появления более широкой поддержки асинхронного программирования в Python.

Асинхронное программирование и сопрограммы в Python

Введение

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

Во второй статье будет рассмотрена реализация сопрограмм при помощи расширенных возможностей генераторов в Python (PEP 342), в третьей мы рассмотрим модуль asyncio (PEP 3156), который стал частью стандартной библиотеки в Python 3.4 и доступен в виде отдельного пакета для Python 3.3, а четвёртая статья цикла будет посвящена асинхронным функциям и сопрограммам в Python 3.5 с использованием нового синтаксиса async/await (PEP 0492).

Понятие асинхронного программирования и сопрограмм

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

Другой известной технологией, основанной на данной модели, является веб-сервер nginx, который часто используется на высоконагруженных проектах, занимая первое место по частоте использования среди 10000 самых посещаемых сайтов в мире (согласно данным W3Techs).


Так что же такое асинхронное программирование и почему оно становится таким популярным, особенно в highload-проектах?

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

Давайте для наглядности рассмотрим это на примере графических интерфейсов пользователя. Что происходит, когда пользователь не производит никаких действий? Ничего. Программа должна ждать, пока пользователь укажет ей, что делать.

Это ожидание можно реализовать в виде постоянных проверок: «а не нажал ли пользователь на кнопку?», «а не поставил ли пользователь курсор в поле ввода?». Таким образом, вычислительные ресурсы тратятся просто на то, чтобы проверить, не случилось ли что-нибудь. К счастью, практически все UI-фреймворки построены иначе. Они реализуют систему обработки событий. Любое действие пользователя – это событие, и разработчик может привязать к нему код – обработчик события.

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

Как правило, ответ – нет. Наиболее распространённая архитектура системы обработки событий – однопоточная, лишь с одним потоком исполнения.

Однопоточные системы обработки событий практически всегда реализуются при помощи очереди событий или сообщений. Идея состоит в том, что, когда программа ничего не делает, её поток смотрит на наличие новых событий в очереди. Когда происходит событие, оно добавляется в очередь и, если поток исполнения в это время не делает ничего другого, он забирает следующее событие из очереди и выполняет соответствующий обработчик.

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

Возникает логичный вопрос: каким же образом событие попадает в очередь, если поток исполнения занят обработкой другого события? Дело в том, что у операционной системы много потоков и тот код, который действительно взаимодействует с пользователем, выполняется отдельно от нашей программы и лишь посылает ей сообщения.

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

Но в данной модели обработчики событий являются неделимыми действиями. И тут возникает проблема: если обработчик событий выполняется слишком долго, интерфейс как бы «подвисает». Причина в том, что пока обработчик события не вернул управление в диспетчер, следующий обработчик не будет выполнен.

Решением является минимизировать количество работы, которое выполняет обработчик события. Но что если ему требуется совершить какие-то вычисления или загрузить данные с сервера? Очевидный ответ – выполнять обработчик этого события в отдельном потоке. Однако в JavaScript есть лишь один поток исполнения, а в Python, как известно, проблемой многопоточных приложений, которая значительно их замедляет, является Global Interpreter Lock (GIL).

Тут мы подходим к тому, что существует два вида многозадачности: вытесняющая и кооперативная.

Потоки и процессы используют вытесняющую многозадачность. Это значит, что операционная система производит квантование времени и постоянно переключается между разными потоками, сохраняя и восстанавливая их контекст выполнения.

При использовании кооперативной многозадачности ветви кода, которые исполняются параллельно, сами отдают управление в определённые моменты времени. Кооперативная многозадачность как способ одновременного выполнения отдельных программ устарела и не используется в современных операционных системах, однако, идеи, заложенные в неё, оказываются очень полезными для организации выполнения асинхронного кода и позволяют при грамотном использовании максимально использовать вычислительные ресурсы в рамках одного потока (а при комбинировании этого подхода с традиционной многопоточностью, как в async/await в C#, можно строить крайне эффективные приложения).

Можно построить обработчик события из множества асинхронных функций обратного вызова (callback-функций), которые управляются общим циклом событий, как это делается в Node.js, однако такой код сложно отлаживать и поддерживать. Значительно упрощают его паттерны Promise и Future, однако Python и некоторые другие языки программирования поддерживают механизм, который позволяет в данном случае обойтись без callback-функций – сопрограммы.

Сопрограмма (coroutine) – это компонент программы, обобщающий понятие подпрограммы, который дополнительно поддерживает множество входных точек (а не одну, как подпрограмма) и остановку и продолжение выполнения с сохранением определённого положения.

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

Async/await: асинхронные возможности в Python 3+

Содержание статьи

Цикл передач на третьем канале

16 марта 2014 года произошло событие, которое привело к довольно бодрым холиварам, — вышел Python 3.4, а вместе с ним и своя внутренняя реализация event loop’а, которую окрестили asyncio. Идея у этой штуки была ровно такая, как я написал во введении: вместо того чтобы зависеть от внешних сишных реализаций отлова неблокирующих событий на сокетах (у gevent — libevent, у Tornado — IOLoop и так далее), почему бы не встроить одну в сам язык?

Сказано — сделано. Теперь бывалые душители змей вместо того, чтобы в качестве ответа на набивший оскомину вопрос «Что такое корутина?» нырять в генераторы и метод .send() , могли ткнуть в красивый декоратор @asyncio.coroutine и отправить вопрошающего читать документацию по нему.

Правда, сами разработчики отнеслись к новой спецификации довольно неоднозначно и с опаской. Хоть код и старался быть максимально совместимым по синтаксису со второй версией языка — проект tulip, который как раз был первой реализацией PEP 3156 и лег в основу asyncio, был даже в каком-то виде бэкпортирован на устаревшую (да-да, я теперь ее буду называть только так) двойку.

Дело было еще и в том, что реализация, при всей ее красоте и приверженности дзену питона, получилась довольно неторопливая. Разогнанные gevent и Tornado все равно оказывались на многих задачах быстрее. Хотя, раз уж в народ в комьюнити настаивал на тюльпанах, в Tornado таки запилили экспериментальную поддержку asyncio вместо IOLoop, пусть она и была в разы медленнее. Но нашлось у новой реализации и преимущество — стабильность. Пусть соединения обрабатывались дольше, зато ответа в итоге дожидалась бОльшая доля клиентов, чем на многих других прославленных фреймворках. Да и ядро при этом, как ни странно, нагружалось чуть меньше.

Старт был дан, да и какой старт! Проекты на основе нового event loop’а начали возникать, как грибы после дождя, — обвязки для клиентов к базам данных, реализации различных протоколов, тысячи их! Появился даже сайт http://asyncio.org/, который собирал список всех этих проектов. Пусть даже этот сайт не открывался на момент написания статьи из-за ошибки DNS — можешь поверить на слово, там интересно. Надеюсь, он еще поднимется.

Но не все сразу заметили, что над новой версией Python завис великий и ужасный PEP 492.

Сегодня в сопрограмме

Так уж получилось, что довольно большое число людей изначально не до конца поняло смысл введения asyncio и считало его чем-то наподобие gevent, то есть сетевым или даже веб-фреймворком. Но суть у него была совсем другая — он открывал новые возможности асинхронного программирования в ядре языка.

Ты же помнишь в общих чертах, что такое генераторы и корутины (они же сопрограммы)? В контексте Python можно привести два определения генераторов, которые друг друга дополняют:

  1. Генераторы — это объекты, предоставляющие интерфейс итератора, то есть запоминающие точку последнего останова, которые при каждом обращении к следующему элементу запускают какой-то ленивый код для его вычисления.
  2. Генераторы — это функции, имеющие несколько точек входа и выхода, заданных с использованием оператора переключения контекста yield.

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

В случае сетевого программирования именно это и позволяет нам быстро опрашивать события на сокете, обслуживая тысячи клиентов сразу. Ну или, в общем случае, мы можем написать асинхронный драйвер для любого I/O-устройства, будь то файловая система на block device или, скажем, воткнутая в USB Arduino.

Да, в ядре Python есть пара библиотек, которые изначально предназначались для похожих целей, — это asyncore и asynchat, но они были, по сути, экспериментальной оберткой над сетевыми сокетами, и код для них написан довольно давно. Если ты сейчас, в начале 2020 года, читаешь эту статью — значит, настало время записать их в музейные экспонаты, потому что asyncio лучше.

Давай забудем на время про несвежий Python 2 и взглянем на реализацию простейшего асинхронного эхо-сервера в Python 3.4:

Нам ничто не мешает подключиться к этому серверу несколькими клиентами и отвечать всем сразу. Это можно проверить, например, с помощью netcat . При этом на сокете будет использоваться лучшая реализация поллинга событий из доступных в системе, в современном Linux это, разумеется, epoll .

Да, этот код асинхронный, но callback hell — тоже вещь довольно неприятная. Немного неудобно описывать асинхронные обработчики как гроздья висящих друг на друге колбэков, не находишь? Отсюда и проистекает тот самый классический вопрос: как же нам, кабанам, писать асинхронный код, который не был бы похож на спагетти, а просто выглядел бы несложно и императивно? На этом месте передай привет в камеру ноутбука (если она у тебя не заклеена по совету ][) тем, кто активно использует Twisted или, скажем, пишет на JavaScript, и поехали дальше.

А теперь давай возьмем Python 3.5 (давно пора) и напишем все на нем.

Красиво? Никаких классов, просто цикл, в котором мы принимаем подключения и работаем с ними. Если этот код сейчас взорвал тебе мозг, то не волнуйся, мы рассмотрим основы этого подхода.

Несложно заметить, что в случае асинхронного программирования подобным образом в питоне все будет крутиться (каламбур) вокруг того самого внутреннего IOLoop’а, который будет связывать события с их обработчиками. Одной из основных проблем, как я уже говорил, остается скорость — связка Python 2 + gevent, которая использует крайне быстрый libev, по производительности показывает гораздо лучшие результаты.

Но зачем держаться за прошлое? Во-первых, есть curio (см. врезку), а во-вторых, уже есть еще одна, гораздо более скоростная реализация event loop’а, написанная как подключаемый плагин для asyncio, — uvloop, основанный на адски быстром libuv.

Что, уже чувствуешь ураганный ветер из монитора?

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», увеличит личную накопительную скидку и позволит накапливать профессиональный рейтинг Xakep Score! Подробнее

Асинхронное программирование в Python

Есть ли общее понятие асинхронного программирования в Python? Могу ли я назначить функцию обратного вызова, выполнить ее и немедленно вернуться к основному потоку программы, независимо от того, сколько времени займет выполнение этой функции?

Отредактировано 14 марта 2020 г.

Описание взято по ссылке выше:

Модуль asyncIO предоставляет инфраструктуру для написания однопоточного параллельного кода с использованием сопрограмм, мультиплексирования доступа ввода-вывода через сокеты и другие ресурсы, запуска сетевых клиентов и серверов и других связанных примитивов. Вот более подробный список содержимого пакета:

  1. подключаемый цикл событий с различными системными реализациями;
  2. абстракции транспорта и протокола (аналогичные тем, что есть в Twisted);
  3. конкретная поддержка TCP, UDP, SSL, каналов подпроцесса, отложенных вызовов и других (некоторые могут зависеть от системы);
  4. класс Future, который имитирует класс в модуле concurrent.futures, но адаптирован для использования с циклом событий;
  5. сопрограммы и задачи, основанные на выходе из (PEP 380), чтобы помочь написать параллельный код в последовательном порядке;
  6. поддержка отмены фьючерсов и сопрограмм;
  7. примитивы синхронизации для использования между сопрограммами в одном потоке, имитируя их в поточном модуле;
  8. интерфейс для передачи работы в пул потоков, когда вам абсолютно необходимо использовать библиотеку, которая блокирует вызовы ввода / вывода.

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

Также стоит проверить:

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


Асинхронное (управляемое событиями) программирование поддерживается в стандартной библиотеке Python в asyncore и asynchat , которые очень ориентированы на сетевые задачи (фактически, они внутренне используют модуль select , который в Windows поддерживает только сокеты — хотя в Unixy ОС также может поддерживать любой дескриптор файла).

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

Хорошие новости всем!

Python 3.4 будет включать новую амбициозную реализацию асинхронного программирования!

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

Люди, которые хотят написать асинхронный код на Python прямо сейчас, имеют несколько вариантов:

  • asyncore и asynchat;
  • что-то сделанное на заказ, скорее всего, на основе выбранного модуля;
  • используя стороннюю библиотеку, такую как Twisted или Gevent .

К сожалению, у каждого из этих вариантов есть свои недостатки, которые пытается решить этот PEP.

Несмотря на то, что модуль asyncore долгое время был частью стандартной библиотеки Python, он страдает фундаментальными недостатками, вытекающими из негибкого API, который не соответствует ожиданиям современного модуля асинхронной сети.

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

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

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

Ключевое слово yield в Python

Python предоставляет программисту большой набор инструментов, один из которых — yield. Он заменяет обычный возврат значений из функции и позволяет сэкономить память при обработке большого объема данных.

yield – один из тех инструментов, использовать которые вовсе не обязательно. Всё, что можно реализовать с его помощью, можно сделать, используя обычный возврат return. Однако этот оператор позволяет не только сэкономить память, но и реализовать взаимодействие между несколькими последовательностями в пределах одного цикла.

Что такое yield и как это работает

Yield – ключевое слово, которое используется вместо return. С его помощью функция возвращает значение без уничтожения локальных переменных, кроме того, при каждом последующем вызове функция начинает своё выполнение с оператора yield.

Функция, содержащая yield в Python 3, называется генератором. Чтобы разобраться, как работает yield и зачем его используют, необходимо узнать, что такое генераторы, итераторы и итерации.

Но перед этим рассмотрим пример:

Тип полученного значения при вызове функции – это генератор. Один из способов получения значений из генератора – это их перебрать в цикле for. Им мы и воспользовались. Но можно его легко привести к списку, как мы сделали в статье про числа Фибоначчи.

Теперь разберемся, как это всё работает.

Что такое итерации

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

Цикл — это повторяющаяся последовательность команд, каждый цикл состоит из итераций. То есть, одно выполнение цикла — это итерация. Например, если тело цикла выполнилось 5 раз, это значит, что прошло 5 итераций.

Итератор — это объект, позволяющий «обходить» элементы последовательностей. Программист может создать свой итератор, однако в этом нет необходимости, интерпретатор Python делает это сам.

Что такое генераторы

Генератор — это обычная функция, которая при каждом своём вызове возвращает объект. При этом в функции-генераторе вызывается next.

Отличие генераторов от обычной функции состоит в том, что функция возвращает только одно значение с помощью ключевого слова return, а генератор возвращает новый объект при каждом вызове с помощью yield. По сути генератор ведет себя как итератор, что позволяет использовать его в цикле for.

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

Помимо yield, есть и другие способы создания генераторов, они описаны в статье о генераторах списка.

Функция next()

Эта функция позволяет извлекать следующий объект из итератора. То есть чтобы цикл перешел с текущей итерации на следующую, вызывается функция next(). Когда в итераторе заканчиваются элементы, возвращается значение, заданное по умолчанию, или возбуждается исключение StopItered.

На самом деле каждый объект имеет встроенный метод __next__, который и обеспечивает обход элементов в цикле, а функция next() просто вызывает его.

Функция имеет простой синтаксис: next(итератор[,значение по умолчанию]) . Она автоматически вызывается интерпретатором Python в циклах while и for.

Вот пример использования next:

Преимущества использования yield

yield используют не потому, что это определено синтаксисом Python, ведь всё, что можно реализовать с его помощью, можно реализовать и с помощью обычного return.

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

Использование yield в языке программирования Python 3 позволяет не сохранять в память всю последовательность, а просто генерирует объект при каждом вызове функции. Это позволяет обойтись без использования большого количества оперативной памяти.

Сравнение производительности return и yield

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

  • Первый использует обычный return, он читает все строки файла и заносит их в список, а затем выводит все строки в консоли.
  • Второй использует yield, он читает по одной строке и возвращает её на вывод.

Затем скрипты должны обработать несколько файлов разных размеров, при этом получаются следующие результаты:

return Память Время Память Время
4 Кбайт 5,3 Мбайт 0.023 с 5,42 Мбайт 0.08 c
324 Кбайт 9,98 Мбайт 0.028 с 5,37 Мбайт 0,32 с
26 Мбайт 392 Мбайт 27 с 5.52 Мбайт 29.61 с
263 Мбайт 3,65 Гбайт 273.56 с 5,55 Мбайт 292,99 с

Видно, что в обоих случаях время увеличивается с примерно одинаковой скоростью, а количество потребляемой памяти сильно различается. Чем больше обрабатываемый файл, тем заметнее различие.

yield from

Многие считают, что yield from был добавлен в язык Python 3, чтобы объединить две конструкции: yield и цикл for, потому что они часто используются совместно, как в следующем примере:

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

yield from позволяет программисту легко управлять сразу несколькими генераторами, настраивать их взаимодействие и, конечно, заменить более длинную конструкцию for+yield, например:

Как видно из примера, yield from позволяет одному генератору получать значения из другого. Этот инструмент сильно упрощает жизнь программиста, особенно при асинхронном программировании.

Заключение

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

yield – это лишь одно из многих полезных средств языка Python, которое может быть без проблем заменено обычным возвратом из функции с помощью return. Оно добавлено в язык, чтобы оптимизировать производительность программы, упростить код и его отладку и дать программистам возможность применять необычные решения в специализированных проектах.

Смешивание асинхронного и синхронного кода в Python


Допустим, есть приложение на flask + socketio + eventlet. Внутри обработчика сообщения от socketio я могу писать какой-нибудь синхронный код. Например, сделать print или отправить ответ клиенту. Для конкретики вот примерный мой код:

Однако, мне нужно работать с библиотекой, которая поддерживает только асинхронный стиль программирования. Не на базе eventlet или чего-то подобного, а на базе непосредственно yield from и т. п. Каким образом я могу делать к ней обращения внутри обработчика сообщения?

AsyncIO для практикующего python-разработчика

Sep 8, 2020 09:27 · 3329 words · 16 minute read перевод python

Оригинал: ‘AsyncIO for the Working Python Developer’ by Yeray Diaz; перевод был впервые опубликован на Хабрахабре.

Я помню тот момент, когда подумал «Как же медленно всё работает, что если я распараллелю вызовы?», а спустя 3 дня, взглянув на код, ничего не мог понять в жуткой каше из потоков, синхронизаторов и функций обратного вызова.

Тогда я познакомился с asyncio, и всё изменилось.

Если кто не знает, asyncio — новый модуль для организации конкурентного программирования, который появился в Python 3.4. Он предназначен для упрощения использования корутин и футур в асинхронном коде — чтобы код выглядел как синхронный, без коллбэков.

Я помню, в то время было несколько похожих инструментов, и один из них выделялся — это библиотека gevent. Я советую всем прочитать прекрасное руководство gevent для практикующего python-разработчика, в котором описана не только работа с ней, но и что такое конкурентность в общем понимании. Мне настолько понравилось та статья, что я решил использовать её как шаблон для написания введения в asyncio.

Небольшой дисклеймер — это статья не gevent vs asyncio. Nathan Road уже сделал это за меня в своей заметке. Все примеры вы можете найти на GitHub.

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

Потоки, циклы событий, корутины и футуры

Потоки — наиболее распространённый инструмент. Думаю, вы слышали о нём и ранее, однако asyncio оперирует несколько другими понятиями: циклы событий, корутины и футуры.

  • цикл событий (event loop) по большей части всего лишь управляет выполнением различных задач: регистрирует поступление и запускает в подходящий момент
  • корутины — специальные функции, похожие на генераторы python, от которых ожидают (await), что они будут отдавать управление обратно в цикл событий. Необходимо, чтобы они были запущены именно через цикл событий
  • футуры — объекты, в которых хранится текущий результат выполнения какой-либо задачи. Это может быть информация о том, что задача ещё не обработана или уже полученный результат; а может быть вообще исключение

Довольно просто? Поехали!

Синхронное и асинхронное выполнение

В видео “Конкурентность — это не параллелизм, это лучше” Роб Пайк обращает ваше внимание на ключевую вещь. Разбиение задач на конкурентные подзадачи возможно только при таком параллелизме, когда он же и управляет этими подзадачами.

Asyncio делает тоже самое — вы можете разбивать ваш код на процедуры, которые определять как корутины, что даёт возможность управлять ими как пожелаете, включая и одновременное выполнение. Корутины содержат операторы yield, с помощью которых мы определяем места, где можно переключиться на другие ожидающие выполнения задачи.

За переключение контекста в asyncio отвечает yield, который передаёт управление обратно в event loop, а тот в свою очередь — к другой корутине. Рассмотрим базовый пример:

  • Сначала мы объявили пару простейших корутин, которые притворяются неблокирующими, используя sleep из asyncio
  • Корутины могут быть запущены только из другой корутины, или обёрнуты в задачу с помощью create_task
  • После того, как у нас оказались 2 задачи, объединим их, используя wait
  • И, наконец, отправим на выполнение в цикл событий через run_until_complete

Используя await в какой-либо корутине, мы таким образом объявляем, что корутина может отдавать управление обратно в event loop, который, в свою очередь, запустит какую-либо следующую задачу: bar. В bar произойдёт тоже самое: на await asyncio.sleep управление будет передано обратно в цикл событий, который в нужное время вернётся к выполнению foo.

Представим 2 блокирующие задачи: gr1 и gr2, как будто они обращаются к неким сторонним сервисам, и, пока они ждут ответа, третья функция может работать асинхронно.

Обратите внимание как происходит работа с вводом-выводом и планированием выполнения, позволяя всё это уместить в один поток. Пока две задачи заблокированы ожиданием I/O, третья функция может занимать всё процессорное время.

Порядок выполнения

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

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

Также обратите внимание на корутину для нашей довольно простой задачи. Это важно для понимания, что в asyncio нет никакой магии при реализации неблокирующих задач. Во время реализации asyncio стоял отдельно в стандартной библиотеке, т.к. остальные модули предоставляли только блокирующую функциональность. Вы можете использовать модуль concurrent.futures для оборачивания блокирующих задач в потоки или процессы и получения футуры для использования в asyncio. Несколько таких примеров доступны на GitHub. Это, наверно, главный недостаток сейчас при использовании asyncio, однако уже есть несколько библиотек, помогающих решить эту проблему.

Самая популярная блокирующая задача — получение данных по HTTP-запросу. Рассмотрим работу с великолепной библиотекой aiohttp на примере получения информации о публичных событиях на GitHub.

Тут стоит обратить внимание на пару моментов.

Во-первых, разница во времени — при использовании асинхронных вызовов мы запускаем запросы одновременно. Как говорилось ранее, каждый из них передавал управление следующему и возвращал результат по завершении. То есть скорость выполнения напрямую зависит от времени работы самого медленного запроса, который занял как раз 0.54 секунды. Круто, правда?

Во-вторых, насколько код похож на синхронный. Это же по сути одно и то же! Основные отличия связаны с реализацией библиотеки для выполнения запросов, созданием и ожиданием завершения задач.

Создание конкурентности

До сих пор мы использовали единственный метод создания и получения результатов из корутин, создания набора задач и ожидания их завершения. Однако, корутины могут быть запланированы для запуска и получения результатов несколькими способами. Представьте ситуацию, когда нам надо обрабатывать результаты GET-запросов по мере их получения; на самом деле реализация очень похожа на предыдущую:

Посмотрите на отступы и тайминги — мы запустили все задачи одновременно, однако они обработаны в порядке завершения выполнения. Код в данном случае немного отличается: мы пакуем корутины, каждая из которых уже подготовлена для выполнения, в список. Функция as_completed возвращает итератор, который выдаёт результаты корутин по мере их выполнения. Круто же, правда?! Кстати, и as_completed, и wait — функции из пакета concurrent.futures.

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

Что ж, для этого в нашей любимой функции wait есть специальный параметр return_when. До сих пор мы игнорировали то, что возвращает wait, т.к. только распараллеливали задачи. Но теперь нам надо получить результат из корутины, так что будем использовать набор футур done и pending.

Что же случилось? Первый сервис ответил успешно, но в логах какое-то предупреждение!

На самом деле мы запустили выполнение двух задач, но вышли из цикла уже после первого результата, в то время как вторая корутина ещё выполнялась. Asyncio подумал что это баг и предупредил нас. Наверно, стоит прибираться за собой и явно убивать ненужные задачи. Как? Рад, что вы спросили.

Состояния футур

  • ожидание (pending)
  • выполнение (running)
  • выполнено (done)
  • отменено (cancelled)

Всё настолько просто. Когда футура находится в состояние done, у неё можно получить результат выполнения. В состояниях pending и running такая операция приведёт к исключению InvalidStateError, а в случае canelled будет CancelledError, и наконец, если исключение произошло в самой корутине, оно будет сгенерировано снова (также, как это сделано при вызове exception). Но не верьте мне на слово.

Вы можете узнать состояние футуры с помощью методов done, cancelled или running, но не забывайте, что в случае done вызов result может вернуть как ожидаемый результат, так и исключение, которое возникло в процессе работы. Для отмены выполнения футуры есть метод cancel. Это подходит для исправления нашего примера.

Простой и аккуратный вывод — как раз то, что я люблю!

Если вам нужна некоторая дополнительная логика по обработке футур, то вы можете подключать коллбэки, которые будут вызваны при переходе в состояние done. Это может быть полезно для тестов, когда некоторые результаты надо переопределить какими-то своими значениями.

Обработка исключений

asyncio — это целиком про написание управляемого и читаемого конкурентного кода, что хорошо заметно при обработке исключений. Вернёмся к примеру, чтобы продемонстрировать. Допустим, мы хотим убедиться, что все запросы к сервисам по определению IP вернули одинаковый результат. Однако, один из них может быть оффлайн и не ответить нам. Просто применим try…except как обычно:

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

Точно также, как и запуск задачи без ожидания её завершения является ошибкой, так и получение неизвестных исключений оставляет свои следы в выводе:

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

Таймауты

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

И снова у wait есть подходящий аргумент:

Я также добавил аргумент timeout к строке запуска скрипта, чтобы проверить что же произойдёт, если запросы успеют обработаться. Также я добавил случайные задержки, чтобы скрипт не завершался слишком быстро, и было время разобраться как именно он работает.

Заключение

Asyncio укрепил мою и так уже большую любовь к python. Если честно, я влюбился в сопрограммы, ещё когда познакомился с ними в Tornado, но asyncio сумел взять всё лучшее из него и других библиотек по реализации конкурентности. Причём настолько, что были предприняты особые усилия, чтобы они могли использовать основной цикл ввода-вывода. Так что если вы используете Tornado или Twisted, то можете подключать код, предназначенный для asyncio!

Как я уже упоминал, основная проблема заключается в том, что стандартные библиотеки пока ещё не поддерживают неблокирующее поведение. Также и многие популярные библиотеки работают пока лишь в синхронном стиле, а те, что используют конкурентность, пока ещё молоды и экспериментальны. Однако, их число растёт.

Надеюсь, в этом уроке я показал, насколько приятно работать с asyncio, и эта технология подтолкнёт вас к переходу на python 3, если вы по какой-то причине застряли на python 2.7. Одно точно — будущее Python полностью изменилось.

От переводчика

Оригинальная статья была опубликована 20 февраля 2020, за это время многое произошло. Вышел Python 3.6, в котором помимо оптимизаций была улучшена работа asyncio, API переведено в стабильное состояние. Были выпущены библиотеки для работы с Postgres, Redis, Elasticsearch и пр. в неблокирующем режиме. Даже новый фреймворк — Sanic, который напоминает Flask, но работает в асинхронном режиме. В конце концов даже event loop был оптимизирован и переписан на Cython, что получилось раза в 2 быстрее. Так что я не вижу причин игнорировать эту технологию!

Мастер Йода рекомендует:  Пишем свою игру в жанре Roguelike
Добавить комментарий