Асинхронные фреймворки python что это

Обновлено: 03.07.2024

Асинхронность уже не является просто модным словечком в сообществе Python. После выпуска библиотеки asyncio в версии 3.5, разработчики Python признали влияние Node.js в сфере веб-разработки и ввели в язык два новых ключевых слова – async и await . Это был крайне важный момент, потому что разработчики максимально осторожно относятся к расширению основного синтаксиса, если только нет острой необходимости, что только указывает на то, насколько принципиально необходимыми считались асинхронные возможности.

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

Пожалуй, хватит мотивации. Давайте лучше глянем на текущую ситуацию в Python и рассмотрим некоторые из лучших асинхронных фреймворков.

На удивление, Tornado – достаточно старый фреймворк. Самый первый релиз был выпущен ещё в далёком 2009 году (более десяти лет назад), и сейчас его основное внимание направлено на обеспечении надежного асинхронного программирования с высоким параллелизмом.

Tornado изначально не являлся веб-фреймворком. На самом деле это просто набор асинхронных модулей, которые используются для построения модулей веб-фреймворка. Если более конкретно, то вот эти модули:

Они были совмещены для получения финальный модулей фреймворка: tornado.web , tornado.routing , tornado.template и тому подобные.

Tornado имеет сильную и преданную поддержку в сообществе Python и используется опытными архитекторами для создания высокоэффективных систем. Это фреймворк, который уже давно имеет ответ на проблемы параллелизма, но не стал мейнстримом, поскольку он не поддерживает стандарт WSGI. А также важно помнить, что основная часть библиотек Python всё ещё синхронна.

Sanic – это "современный" фреймворк в прямом смысле слова: он не работает в Python ниже версии 3.6, поддерживает синтаксис async/await из коробки, и как следствие, это не заставляет вас читать кучу документации и держать в голове все крайние случаи, прежде чем вы сможете написать свой первый обработчик.

В результате получившийся синтаксис довольно приятен; он напоминает код, который вы написали бы на любом другом микрофреймворке (например, Flask, CherryPy) просто с несколькими асинхронными вставками:

Sanic, пожалуй, является самым популярным и наиболе понравившемся сообществу асинхронным фреймворков в мире Python. Он имеет практически весь функционал, который вам понадобился бы для проекта – маршрутизация, middleware, куки, контроль версий, блупринты, представления на основе классов, статические файлы, streaming, сокеты и многое другое. А то, что он не предоставляет из коробки – шаблоны, поддержка баз данных, I/O операции с файлами, очереди – вы можете добавить сами, так как существует уже достаточно асинхронных библиотек для всего, что душа пожелает.

Фреймворк Vibora крайне похож на Sanic, за исключением того, что он претендует на звание самого быстрого веб-сервера на Python. Более того, при самом первом посещении официального сайта можно увидеть график сравнения с другими фреймворками:

Как вы можете видеть, Vibora заявляет, что она в несколько раз быстрее классических фреймворков и более чем в два раза быстрее Sanic, своего ближайшего конкурента. Конечно, бенчмарки не всегда могут быть объективными показателями. 🙂

Хотя по синтаксису и функциям Vibora сравнима с Sanic (или, возможно, даже немного лучше, поскольку она объединяет популярные библиотеки и такие вещи, как шаблоны, доступные из коробки), я бы счел Sanic более зрелым, поскольку он существует дольше и имеет более обширное сообщество.

Но если вы помешаны на производительности, то Vibora может дать вам большие возможности. Тем не менее, на момент написания статьи Vibora кардинально переписывается, чтобы стать ещё быстрее, и ссылка на страницу про производительность говорит нам о том, что фреймворк находится под “тяжелой разработкой". Однако не спешите выбирать Vibora для своих проектов, иначе вы рано или поздно столкнётесь с критическими изменениями. Но всё-таки это только начало асинхронного мира Python, и не следует ожидать чего-либо железно стабильного.

Если вам приятно разрабатывать на Flask, но не хватает асинхронной поддержки, то вам определнно понравится Quart.

Выглядит (почти) как Flask, разве не так?

Поскольку Quart – это эволюция Flask, то и весь функционал из Flask также доступен: маршрутизация, middleware, сессии, шаблоны, блупринты и так далее. Более того, вы даже можете использовать расширения Flask непосредственно внутри Quart. Загвоздка заключается лишь в том, что поддерживается Python только версии 3.7+, но если вы не используете последнюю версию Python, то и использовать асинхронность имеет мало смысла. 🙂

Документация действительно нужна, если у вас нет предварительного опыта работы с Flask, но я могу всё-таки рекомендовать Quart, поскольку это, вероятно, единственный асинхронный фреймворк, который скоро выйдет в версии 1.0.

Последним (но самым впечатляющим) фреймворком в этом списке является FastAPI. Нет, это не только API-фреймворк; на самом деле FastAPI, похоже, является самым многофункциональным и богатым документацией фреймворком, с которым я столкнулся при исследовании асинхронных фреймворков в Python.

Интересно, что автор фреймворка глубоко изучил несколько других фреймворков, от классических, таких как Django, до более современных, таких как Sanic, а также изучил различные технологии в NestJS (веб-фреймворк Node.js, Typescript). Про их философию развития и масштабные сравнения можно прочитать здесь.

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

А теперь представлю список убийственных функций, которые заставляют FastAPI затмевать другие фреймворки:

API автоматической генерации документации: как только ваши конечные точки будут реализованы, вы сможете поиграться с API, используя соответствующий стандартам пользовательский интерфейс. Поддерживаются SwaggerUI, ReDoc и другие.

Фреймворк также создаёт автоматическую документацию для моделей в JSON формате.

Современное развитие: да, слово "современный" часто бросается в глаза, но именно в контексте FastAPI оно уместно. Внедрение зависимостей и аннотация типов – это объекты первого класса, не только позволяющие применять хорошие принципы программирования, но и предотвращающие ошибки и путаницу в долгосрочной перспективе.

Обширная документация: не знаю насчёт вас, но я большой любитель хорошей документации. И в этой области FastAPI преуспел лучше других. В нем есть страницы за страницами документации, объясняющие почти каждую тонкость и моменты, в которых нужно быть осторожным, для разработчиков всех уровней. Сразу видно, что в такую документацию вложили сердце и душу. И единственное сравнение, которое можно найти, – это документация Django (да, документация FastAPI настолько хороша).

Помимо всего прочего: FastAPI поддерживает WebSockets, Streaming, а также GraphQL, помимо того, что у него есть все традиционные функции, такие как CORS, сессии, куки и многие другие.

А как насчет производительности? FastAPI построен на удивительной библиотеке Starlette, в результате чего производительность сравнима — с Node.js, а в некоторых случаях даже Go! В целом, у меня сложилось стойкое предчуствие, что FastAPI будет мчаться вперёд как лучший асинхронный фреймворк для Python.

Заключение

В наши дни очень много событий происходит в асинхронном мире Python. Появляются новые фреймворки, старые переписываются, а библиотеки эволюционируют, чтобы соответствовать асинхронным концепциям. Несмотря на то, что Python имеет встроенную поддержку событийного цикла и можно сделать части вашего приложения асинхронными, у вас всё ещё есть отличная возможность пойти ва-банк и построить приложение на одном из вышеперечисленных фреймворков. Просто не забывайте о долгосрочной перспективе: некоторые из существующих асинхронных фреймворков Python находятся на ранних стадиях и быстро развиваются, что может повредить вашему процессу разработки и повысить бизнес-затраты. Главное – осторожность!

Асинхронное программирование теперь является первоклассным гражданином в Python. Если вы веб-разработчик, вы можете выбрать топ-5 изумительных фреймворков.

На момент написания статьи, асинхронность больше не была просто модным словом в сообществе Python. Выпустив свою библиотеку asyncio в версии 3.5, Python признал влияние Node.js на веб-разработку и ввел два новых ключевых слова в язык — async иawait. Это было очень большое дело, поскольку язык Python крайне осторожен в расширении основного синтаксиса .

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

Но достаточно мотивации!

Давайте рассмотрим некоторые из лучших асинхронных фреймворков на Python.

1. Tornado

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

Tornado принципиально не является веб-фреймворком. Это коллекция асинхронных модулей, которые также используются для создания модуля веб-фреймворка. Более конкретно, эти модули:

Они были объединены для получения окончательных каркасных модулей: tornado.web, tornado.routing, tornado.templateи т.д.

import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()

Tornado имеет сильных и преданных последователей в сообществе Python и используется опытными архитекторами для создания высокопроизводительных систем. Это фреймворк, который долгое время отвечал на проблемы параллелизма, но, возможно, не стал мейнстримом, поскольку не поддерживает стандарт WSGI и был слишком активным участником (помните, что большинство библиотек Python по-прежнему синхронны ).

2. Sanic

Синтаксис в Sanic довольно приятен. Он напоминает код, который вы бы написали с помощью любой другой микросхемы (например, Flask, CherryPy), добавив всего несколько async:

Sanic, пожалуй, самая популярная асинхронная среда в мире Python. Он имеет почти все функции, необходимые для множества проектов — маршрутизация, промежуточное ПО, файлы cookie, управление версиями, чертежи, представления на основе классов, статические файлы, потоковая передача, сокеты и то, что он не предлагает «из коробки». — шаблонизатор, поддержка базы данных, файловый ввод / вывод, очереди и т.д.

3. Vibora

Vibora — близкий родственник Sanic, за исключением того, что он намерен стать самым быстрым веб-сервером Python.

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

Зачем нужна асинхронность?

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

С синхронным в целом все понятно — пришел клиент, открылся сокет, передали данные, если это все — сокет закрылся. В этом случае пока мы не закончили локальный диалог с одним клиентом — не можем начать его с другим. По такому принципу обычно работают простые серверы, которым не надо держать сотни и тысячи клиентов. В случае если нагрузка возрастает, но не критично — можно создать еще один или несколько потоков (или даже процессов) и обрабатывать подключения еще и в них. Это обкатанный годами, стабильно работающий подход, который, например, использует сервер Apache, — никаких неожиданностей, данные от клиентов обрабатываются в порядке строгой очереди, а в случае запуска какого-то «долгого» кода — например, каких-то вычислений или хитрого запроса в БД — это все никак не влияет на других клиентов.

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

Все началось с системных вызовов

Собственно, вариантов системных вызовов для неблокирующей работы с сетевым вводом-выводом не так уж и много (хотя они слегка и разнятся от платформы к платформе). Самый первый, базовый, можно сказать ветеран — это системный вызов select(), который появился еще в бородатые восьмидесятые годы вместе с первой версией того, что сейчас называется POSIX-сокетами (то есть сокетами в понимании большинства современных серверных систем), а тогда называлось Berkeley sockets, сокетами Беркли.

По большому счету, во времена описания системного вызова select() вообще мало кто задумывался о том, что когда-то приложения могут стать НАСТОЛЬКО высоконагруженными. Фактически все, что этот вызов умеет делать, — принимать фиксированное количество (по умолчанию не более 1024) однозначно описанных в программе файловых дескрипторов и слушать события на них. При готовности дескриптора к чтению или записи запустится соответствующий колбэк-метод в коде.

Почему select() все еще существует и используется?

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

Потом кто-то задумался о том, что неплохо бы все-таки научиться делать действительно по-взрослому высоконагруженные сетевые приложения, и появился системный вызов poll(). Кстати, в Linux он существует довольно давно, а вот в Windows его не было до выпуска Windows Vista. Вместо разрозненных сокетов этот вызов принимает на вход структуру со списком дескрипторов (фактически произвольного размера, без ограничений) и возможных событий на них. Затем система начинает в цикле бегать по этой структуре и отлавливать события.

Главный минус вызова poll() (хотя это, несомненно, был большой шаг вперед по сравнению с select()) — обход структуры с дескрипторами с точки зрения алгоритмики линеен, то есть осуществляется за O(n). Причем это касается не только отслеживания событий, но и реакции на них, да еще и надо передавать информацию туда-обратно из kernel space в user space.

А вот дальше в каждой операционной системе решили пойти своим путем. Нельзя сказать, что подходы конкретно различаются, но все-таки реализовать кросс-платформенную асинхронную работу с сокетами в своей программе стало чуточку сложнее. Под Windows появился API работы с так называемыми IO Completion Ports, в BSD-системах добавили механизм kqueue/kevent, а в Linux, начиная с ядра 2.5.44, стал работать системный вызов epoll. Отлов асинхронных событий на сокетах (как бы тавтологично это ни звучало) стал асинхронным сам по себе, то есть вместо обхода структур операционная система умеет подавать сигнал о событии в программу практически моментально после того, как это событие произошло. Кроме того, сокеты для мониторинга стало можно добавлять и убирать в любой момент времени. Это и есть те самые технологии, которые сегодня широко используются в большинстве сетевых фреймворков.

Зоопарк event loop’ов

Основная идея программирования с использованием вышеописанных штук состоит в том, что на уровне приложения реализуется так называемый event loop, то есть цикл, в котором непосредственно происходит отлов событий и дергаются callback’и. Под *nix-системами давненько уже существуют обертки, которые позволяют максимально упростить работу с сокетом и абстрагировать написанный код от низкоуровневой системной логики. Например, существует известная библиотека libevent, а также ее младшая сестра libev. Эти библиотеки собираются под разные системы и позволяют использовать самый совершенный из доступных механизмов мониторинга событий.

Я буду приводить в пример большей частью пакеты для сетевого программирования на языке Python, ибо их действительно там целый зоопарк на любой вкус, а еще они популярны и широко используются в различных проектах. Даже в самом языке довольно давно уже существуют встроенные модули asyncore и asynchat, которые, хоть и не умеют работать с epoll (только select/poll), вполне подходят для написания своих реализаций протоколов.

Одна из проблем сетевых библиотек заключается в том, что в каждой из них написана своя имплементация event loop’а, поэтому, даже несмотря на общий подход, перенос, скажем, плагина для Twisted (Reactor) на Tornado (IOLoop) или наоборот может оказаться вовсе не тривиальной задачей. Эту проблему призван решить новый встроенный модуль в Python 3.4, который называется asyncio и, вопреки расхожему мнению, не является сетевой библиотекой или веб-фреймворком в полном смысле слова, а является именно что встроенной в язык реализацией event loop’а. Эта штука как раз и призвана сплотить сторонние библиотеки вокруг одной общей стабильной технологии. Если хочется немного подробностей и независимых впечатлений об asyncio — милости прошу сюда.

Для Tornado уже существует реализация поддержки event loop’а из asyncio, и, более того, она не так давно вышла из состояния беты. Посмотреть можно здесь. Для Twisted релиз asyncio тоже не оказался неожиданностью, и его разработчики даже написали своеобразный шутливый некролог для проекта, в котором, напротив, уверяют, что это вовсе не конец, а очень даже начало новой эпохи развития.
Если говорить уж совсем откровенно, то понятие асинхронного ввода-вывода необязательно должно относиться именно к сетевому сокету. Системы семейства *nix следуют принципу, согласно которому взаимодействие фактически с любым устройством или сервисом происходит через file-like объект. Примерами таких объектов могут служить UNIX-сокеты или, скажем, ноды псевдофайловой системы /dev, через которые осуществляется обмен информацией с блочными устройствами. Соответственно, говоря об event loop’ах, мы можем подразумевать не только сетевое взаимодействие, но и просто любой асинхронный I/O. А чтобы было что потрогать руками — советую глянуть, например, вот на этот встроенный модуль из Python 3.4.

Запутанная история

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

Как я уже упомянул выше, реализация event loop’а в Twisted называется Reactor. Суть работы с ним состоит в том, что мы регистрируем callback’и, которые выполняются в глобальном цикле в виде реакции на какие-то события. Выглядеть это может, например, так:

Кстати, нельзя не уточнить, что Twisted по умолчанию однопоточный, то есть вся эта развесистая петрушка реализована на внутренней магии вокруг системных вызовов для асинхронного I/O. Но на случай крайней нужды в нем есть и своя реализация ThreadPool, которая добавляет возможность работы нескольких потоков.

Сюда идет Tornado

Twisted возник на горизонте раньше Tornado, однако тогда еще не начался этот бум на асинхронные веб-приложения, а когда он все-таки пришел, то Twisted оказался слегка не у дел в этой сфере, потому что изначально смотрел немного в другую сторону. Это выразилось в том, что веб-приложения на Twisted сейчас в основном пишут только приверженцы старой школы, а для Tornado появилось довольно большое число библиотек, которые добавляют, например, асинхронную работу с базами данных и key-value хранилищами, удобную интеграцию с фронтенд-технологиями наподобие SockJS и SocketIO и все такое прочее. В результате он сейчас является прямым конкурентом Node.js, только из мира Python.

В качестве примера асинхронного подхода рассмотрим такой код:

Про то, что такое корутины и как они работают, можно прочитать в моей статье в октябрьском номере. Этот код можно считать примером простейшего асинхронного приложения на Tornado — запускается сервер на 9999-м порту, который при заходе по URL "/test" запускает отложенную таску, в которой каждую секунду шлет следующее число из счетчика в сокет, при этом не забывая обрабатывать другие подключения.

Освещаем события

Асинхронные серверы — это круто, но как же насчет асинхронных клиентов? Такие тоже писать довольно легко. В Python это можно делать с использованием одной из двух довольно известных библиотек — gevent и eventlet. На их основе создаются отличные скоростные парсеры и системы мониторинга, которые по-настоящему быстро опрашивают тысячи серверов.

Нет, на самом деле серверы с их помощью тоже можно писать. Например, в известной облачной open source платформе OpenStack eventlet используется как база при построении REST-сервисов в некоторых подпроектах. Но в этих библиотеках также присутствует действительно хорошая инфраструктура для написания клиентов.

Gevent работает в основном с библиотекой libevent (или, в новых версиях, libev), а eventlet может при желании работать и просто с epoll. Основная задача этих модулей — создание удобной инфраструктуры для работы с корутинами и запуск тасков в «зеленом» режиме, то есть реализация кооперативной многозадачности за счет быстрого переключения контекста.

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

В качестве примера кода с переключением контекста приведу код из примеров стандартной библиотеки gevent:

А вот первый же пример простейшего асинхронного клиента на eventlet (его и другие примеры можно найти на официальном сайте):

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

И что в итоге?

С одной стороны, асинхронный подход к программированию крайне полезен при решении целого ряда задач, особенно в сфере веб-разработки. С другой — он зачастую требует вывернуть мозг наизнанку, так как любая непродуманная строчка кода может привести к блокировке всего и вся. Но, несомненно, знание того, как работают подобные вещи, может быть очень полезным для современного разработчика, особенно в свете развития таких языков, как Go и Erlang, которые внутри себя скрещивают сразу несколько видов асинхронности и многопоточности в одном флаконе. Поэтому — категорически рекомендую пробовать, ошибаться, радоваться и вообще программировать. Удачи!

По очереди

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

Потоки дают возможность вашей программе выполнять ряд задач одновременно. Конечно, у потоков есть ряд недостатков. Многопоточные программы являются более сложными и, как правило, более подвержены ошибкам. Они включают в себя такие проблемы: состояние гонки (race condition), взаимная (deadlock) и активная (livelock) блокировка, исчерпание ресурсов (resource starvation).

Переключение контекста

Хотя асинхронное программирование и позволяет обойти проблемные места потоков, оно было разработано для совершенно другой цели — для переключения контекста процессора. Когда у вас есть несколько потоков, каждое ядро процессора может запускать только один поток за раз. Для того, чтобы все потоки/процессы могли совместно использовать ресурсы, процессор очень часто переключает контекст. Чтобы упростить работу, процессор с произвольной периодичностью сохраняет всю контекстную информацию потока и переключается на другой поток.

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

Эффективный секретарь

Теперь давайте рассмотрим эти понятия на примерах из жизни. Представьте секретаря, который настолько эффективен, что не тратит время впустую. У него есть пять заданий, которые он выполняет одновременно: отвечает на телефонные звонки, принимает посетителей, пытается забронировать билеты на самолет, контролирует графики встреч и заполняет документы. Теперь представьте, что такие задачи, как контроль графиков встреч, прием телефонных звонков и посетителей, повторяются не часто и распределены во времени. Таким образом, большую часть времени секретарь разговаривает по телефону с авиакомпанией, заполняя при этом документы. Это легко представить. Когда поступит телефонный звонок, он поставит разговор с авиакомпанией на паузу, ответит на звонок, а затем вернется к разговору с авиакомпанией. В любое время, когда новая задача потребует внимания секретаря, заполнение документов будет отложено, поскольку оно не критично. Секретарь, выполняющий несколько задач одновременно, переключает контекст в нужное ему время. Он асинхронный.

Потоки — это пять секретарей, у каждого из которых по одной задаче, но только одному из них разрешено работать в определенный момент времени. Для того, чтобы секретари работали в потоковом режиме, необходимо устройство, которое контролирует их работу, но ничего не понимает в самих задачах. Поскольку устройство не понимает характер задач, оно постоянно переключалось бы между пятью секретарями, даже если трое из них сидят, ничего не делая. Около 57% (чуть меньше, чем 3/5) переключения контекста были бы напрасны. Несмотря на то, что переключение контекста процессора является невероятно быстрым, оно все равно отнимает время и ресурсы процессора.

Зеленые потоки

Зеленые потоки (green threads) являются примитивным уровнем асинхронного программирования. Зеленый поток — это обычный поток, за исключением того, что переключения между потоками производятся в коде приложения, а не в процессоре. Gevent — известная Python-библиотека для использования зеленых потоков. Gevent — это зеленые потоки и сетевая библиотека неблокирующего ввода-вывода Eventlet. Gevent.monkey изменяет поведение стандартных библиотек Python таким образом, что они позволяют выполнять неблокирующие операции ввода-вывода. Вот пример использования Gevent для одновременного обращения к нескольким URL-адресам:

Как видите, API-интерфейс Gevent выглядит так же, как и потоки. Однако за кадром он использует сопрограммы (coroutines), а не потоки, и запускает их в цикле событий (event loop) для постановки в очередь. Это значит, что вы получаете преимущества потоков, без понимания сопрограмм, но вы не избавляетесь от проблем, связанных с потоками. Gevent — хорошая библиотека, но только для тех, кто понимает, как работают потоки.

ABBYY , Москва, можно удалённо , До 230 000 ₽

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

Функция обратного вызова (callback)

В Python много библиотек для асинхронного программирования, наиболее популярными являются Tornado, Asyncio и Gevent. Давайте посмотрим, как работает Tornado. Он использует стиль обратного вызова (callbacks) для асинхронного сетевого ввода-вывода. Обратный вызов — это функция, которая означает: «Как только это будет сделано, выполните эту функцию». Другими словами, вы звоните в службу поддержки и оставляете свой номер, чтобы они, когда будут доступны, перезвонили, вместо того, чтобы ждать их ответа.
Давайте посмотрим, как сделать то же самое, что и выше, используя Tornado:

Сравнения

Если вы хотите предотвратить блокировку ввода-вывода, вы должны использовать либо потоки, либо асинхронность. В Python вы выбираете между зелеными потоками и асинхронным обратным вызовом. Вот некоторые из их особенностей:

Зеленые потоки

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

Обратный вызов

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

Как решить эти проблемы?

Вплоть до Python 3.3 зеленые потоки и обратный вызов были оптимальными решениями. Чтобы превзойти эти решения, нужна поддержка на уровне языка. Python должен каким-то образом частично выполнить метод, прекратить выполнение, поддерживая при этом объекты стека и исключения. Если вы знакомы с концепциями Python, то понимаете, что я намекаю на генераторы. Генераторы позволяют функции возвращать список по одному элементу за раз, останавливая выполнение до того момента, когда следующий элемент будет запрошен. Проблема с генераторами заключается в том, что они полностью зависят от функции, вызывающей его. Другими словами, генератор не может вызвать генератор. По крайней мере так было до тех пор, пока в PEP 380 не добавили синтаксис yield from , который позволяет генератору получить результат другого генератора. Хоть асинхронность и не является главным назначением генераторов, они содержат весь функционал, чтобы быть достаточно полезными. Генераторы поддерживают стек и могут создавать исключения. Если бы вы написали цикл событий, в котором бы запускались генераторы, у вас получилась бы отличная асинхронная библиотека. Именно так и была создана библиотека Asyncio.

Все, что вам нужно сделать, это добавить декоратор @coroutine , а Asyncio добавит генератор в сопрограмму. Вот пример того, как обработать те же три URL-адреса, что и раньше:

Несколько особенностей, которые нужно отметить:

  • ошибки корректно передаются в стек;
  • можно вернуть объект, если необходимо;
  • можно запустить все сопрограммы;
  • нет обратных вызовов;
  • строка 10 не выполнится до тех пор, пока строка 9 не будет полностью выполнена.

Единственная проблема заключается в том, что объект выглядит как генератор, и это может вызвать проблемы, если на самом деле это был генератор.

Async и Await

Библиотека Asyncio довольно мощная, поэтому Python решил сделать ее стандартной библиотекой. В синтаксис также добавили ключевое слово async . Ключевые слова предназначены для более четкого обозначения асинхронного кода. Поэтому теперь методы не путаются с генераторами. Ключевое слово async идет до def , чтобы показать, что метод является асинхронным. Ключевое слово await показывает, что вы ожидаете завершения сопрограммы. Вот тот же пример, но с ключевыми словами async / await:

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

Заключение

В Python встроена отличная асинхронная библиотека. Давайте еще раз вспомним проблемы потоков и посмотрим, решены ли они теперь:

  • процессорное переключение контекста: Asyncio является асинхронным и использует цикл событий. Он позволяет переключать контекст программно;
  • состояние гонки: поскольку Asyncio запускает только одну сопрограмму и переключается только в точках, которые вы определяете, ваш код не подвержен проблеме гонки потоков;
  • взаимная/активная блокировка: поскольку теперь нет гонки потоков, то не нужно беспокоиться о блокировках. Хотя взаимная блокировка все еще может возникнуть в ситуации, когда две сопрограммы вызывают друг друга, это настолько маловероятно, что вам придется постараться, чтобы такое случилось;
  • исчерпание ресурсов: поскольку сопрограммы запускаются в одном потоке и не требуют дополнительной памяти, становится намного сложнее исчерпать ресурсы. Однако в Asyncio есть пул «исполнителей» (executors), который по сути является пулом потоков. Если запускать слишком много процессов в пуле исполнителей, вы все равно можете столкнуться с нехваткой ресурсов.

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

Существует несколько вариантов асинхронного программирования в Python. Вы можете использовать зеленые потоки, обратные вызовы или сопрограммы. Хотя вариантов много, лучший из них — Asyncio. Если используете Python 3.5, то вам лучше использовать эту библиотеку, так как она встроена в ядро ​​python.

Читайте также: