В ожидании будущего

Асинхронное программирование на python в последнее время становится все более популярным. В python есть много разных библиотек для асинхронного программирования. Одна из этих библиотек - asyncio, стандартная библиотека Python, добавленная в Python 3.4. Asyncio - одна из причин, по которой асинхронное программирование становится все более популярным в Python. Эта статья объяснит, что такое асинхронное программирование, и сравнит некоторые из этих библиотек. Давайте пройдемся по истории и посмотрим, как асинхронное программирование эволюционировало в Python.

Один за раз

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

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

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

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

Асинхронное программирование - это, по сути, потоки программного обеспечения / пользовательского пространства, при котором приложение управляет потоками и переключением контекста, а не ЦП. По сути, в асинхронном мире контекст переключается только в определенных точках переключения, а не в недетерминированных интервалах.

Невероятно эффективный секретарь

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

Потоковая версия этого будет выглядеть как 5 Бобов, каждый из которых имеет только одну задачу, но только одному разрешено работать в любой момент времени. Было бы устройство, которое контролирует, с чем может работать Боб, которое ничего не понимает в самих задачах. Поскольку устройство не понимает событийного характера задач, оно будет постоянно переключаться между 5 Бобами, даже если трое из них сидят и ничего не делают. Например, Paper-Filing-Bob прерывается, так что Phone-Call-Bob может выполнять некоторую работу, но Phone-Call-Bob не имеет ничего общего, поэтому он просто снова засыпает. На переключение между всеми Бобами тратится время, чтобы узнать, что трое из них даже ничего не делают. Около 57% (чуть меньше 3/5) переключения контекста будут напрасными. И хотя да, переключение контекста ЦП невероятно быстрое, ничего бесплатного.

Зеленые нити

Зеленые потоки - это примитивный уровень асинхронного программирования. Зеленый поток выглядит и ощущается как обычный поток, за исключением того, что потоки планируются кодом приложения, а не оборудованием. Gevent - хорошо известная библиотека Python для использования зеленых потоков. Gevent - это в основном зеленые потоки + eventlet, неблокирующая сетевая библиотека ввода-вывода. Gevent Monkey исправляет общие библиотеки Python, чтобы иметь неблокирующий ввод-вывод. Вот пример использования gevents для одновременного запроса нескольких URL:

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

Цикл событий? Сопрограммы? Ого, помедленнее, я заблудился ...

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

Асинхронный стиль обратного вызова

Хотя в Python существует множество асинхронных библиотек, наиболее популярными из них, вероятно, являются Tornado и gevent. Поскольку мы уже говорили о gevent, давайте немного сосредоточимся на том, как работает Tornado. Tornado - это асинхронная веб-платформа, которая использует стиль обратного вызова для выполнения асинхронного сетевого ввода-вывода. Обратный вызов - это функция, и это означает: «Как только это будет сделано, выполните эту функцию». По сути, это ловушка для вашего кода. Другими словами, обратный звонок похож на тот, когда вы звоните в службу поддержки клиентов, сразу оставляете свой номер и кладете трубку, чтобы они могли перезвонить вам, когда они будут доступны, вместо того, чтобы ждать вечно в ожидании.

Давайте посмотрим, как сделать то же самое, используя торнадо.

Чтобы немного объяснить код, в самой последней строке вызывается метод торнадо под названием AsyncHTTPClient.fetch, который извлекает URL-адрес неблокирующим способом. Этот метод по существу выполняется и немедленно возвращается, позволяя программе делать другие вещи, ожидая сетевого вызова. Поскольку следующая строка достигается до того, как URL-адрес был достигнут, невозможно получить возвращаемый объект из метода. Решение этой проблемы состоит в том, что вместо метода fetch, возвращающего объект, он вызывает функцию с результатом или обратный вызов . Обратным вызовом в этом примере является handle_response.

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

В предыдущем примере вы заметите, что самая первая строка проверяет наличие ошибки. Это необходимо, потому что невозможно вызвать исключение. Если возникнет исключение, оно не будет обработано соответствующим разделом кода из-за цикла событий. Когда выполняется fetch, он запускает http-вызов, а затем помещает обработку ответа в цикл событий. К тому времени, когда мы заметим нашу ошибку, стек вызовов будет состоять только из цикла событий и этой функции, и ни один из наших кодов не будет обрабатывать исключение. Таким образом, любые исключения, возникшие в обратном вызове, нарушат цикл обработки событий и программу. Поэтому все ошибки должны передаваться как объекты, а не подниматься. Это означает, что если вы забудете проверить наличие ошибок, ваши ошибки будут проглочены. Любой, кто знаком с голангом, узнает этот стиль, поскольку язык навязывает его повсюду. Это наиболее часто встречающийся аспект голанга.

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

Сравнения

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

Стиль Зеленых Нитей

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

Стиль обратного звонка

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

Как мы можем улучшить?

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

Здесь следует отметить пару моментов:

  1. Мы не ищем ошибок, потому что ошибки передаются по стеку правильно.
  2. Мы можем вернуть объект, если захотим.
  3. Мы можем запустить все сопрограммы и собрать их позже.
  4. Без обратных вызовов
  5. Строка 10 не выполняется, пока строка 9 не будет полностью завершена. (кажется синхронным / знакомым)

Жизнь замечательна! Единственная проблема заключается в том, что yield from слишком похож на генератор, и это могло бы вызвать проблемы, если бы это действительно был генератор.

Асинхронный и ожидающий

Библиотека asyncio набирала популярность, поэтому Python решил сделать ее базовой библиотекой. С введением основной библиотеки они также добавили ключевые слова async и await в Python 3.5. Ключевые слова предназначены для того, чтобы прояснить, является ли ваш код асинхронным; так что ваши методы не путают с генераторами. Ключевое слово async стоит перед def, чтобы показать, что метод является асинхронным. Ключевое слово await заменяет yield from и проясняет, что вы ждете завершения сопрограммы. Вот снова наш пример, но с ключевыми словами async / await.

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

Мы приехали

Наконец-то у Python появился отличный асинхронный фреймворк asyncio. Давайте посмотрим на все проблемы многопоточности и посмотрим, решили ли мы их.

  • Переключение контекста ЦП: asyncio является асинхронным и использует цикл событий; это позволяет вам иметь управляемые приложением переключатели контекста во время ожидания ввода-вывода. Переключение ЦП здесь не обнаружено!
  • Условия гонки: поскольку asyncio запускает только одну сопрограмму за раз и переключается только в определенных вами точках, ваш код защищен от условий гонки.
  • Тупиковые / динамические блокировки: поскольку вам не нужно беспокоиться об условиях гонки, вам вообще не нужно использовать блокировки. Это позволяет избежать тупиковых ситуаций. Вы все равно можете попасть в ситуацию тупиковой блокировки, если вам потребуются две сопрограммы для пробуждения друг друга, но это настолько редко, что вам почти придется попытаться заставить это случиться.
  • Истощение ресурсов: поскольку все сопрограммы выполняются в одном потоке и не требуют дополнительных сокетов или памяти, было бы намного сложнее исчерпать ресурсы. Asyncio, однако, имеет «пул исполнителей», который по сути является пулом потоков. Если бы вы запускали слишком много вещей в пуле исполнителей, у вас все равно могли бы исчерпаться ресурсы. Однако использование слишком большого количества исполнителей - это антипаттерн, и вы, вероятно, не будете делать это очень часто.

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

Это все, ребята

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

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