Найти тему

Async IO’s Roots. Python

Оглавление

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

Первая часть

Вторая часть

__________________________________________________________________________________________

Async IO’s берет свое начало из генераторов

Ранее мы с вами уже видели старые корутины, основанные на генераторах. Скоро они будут заменены натуральными корутинами, но пример стоит показать еще раз, с небольшим изменением:

Проверим в качестве эксперимента, что мы получим, если просто вызовем py34_coro() или py35_coro() без await или asyncio.run(). Вызов изолированной корутины возвращает объект-корутину:

-2

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

Настало время загадки: что в Python обладает схожим поведением?

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

-3

Функции-генераторы - это основа async IO. (вне зависимости от того, что мы объявляем корутины с помощью async, а не устаревшего декоратора @asyncio.coroutinewrapper). Технически, наиболее близкой аналогией к await будет yield from, а не yield. (Помните, что yield from x() это просто синтаксический сахар для замены for i in x(): yield i.)

Одна из самых важных для async IO возможностей генераторов - способность останавливать и возобновлять исполнение. Например, вы можете прервать итерирование по объекту-генератору, выполнить какое-то действие и вернуться к процессу. Когда генератор достигает yield, он возвращает текущее значение и ожидает нового обращения, чтобы начать расчет следующего элемента.

Разберем на примере:

-4

Такое же поведение задает слово await: оно обозначает точку, в которой корутина замирает и отдает ресурсы другим корутинам. "Замирает" - значит, что корутина временно отдала контроль над ресурсами но еще не истощена или завершена. Просто держим в уме, что yield, await и yield from обозначают точку "замирания" для исполнения генератора.

Это фундаментальное различие между функцией и генератором. Подход функций можно описать как "все-или-ничего". Однажды начав работу, она не остановится до возвращения какого-либо результата. С другой стороны, генератор останавливается каждый раз, когда доходит до команды yield. Он может не только вернуть значение, но и сохранить локальные переменные при следующем вызове next().

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

Если вы заинтересовались и хотите узнать больше, то можете начать с введения в корутины в PEP 342. Затем можно обратиться к the Heck Does Async-Await Work in Python или PYMOTW writeup on asyncio Бретта Кеннона. Для более глубокого разбора механизмов, использующихся в корутинах можно изучить курс Дэвида Бизли Curious Course on Coroutines and Concurrency.

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

Несколько главных тезисов о представлении корутин как генераторов:

  • корутины - многоцелевые генераторы, которые используют преимущества подхода генераторов.
  • старые корутины, основанные на генераторах, использовали yield from, чтобы получить результат корутины. Новые синтаксис и натуральные корутины просто заменили yield from на await. Await это аналогия yield from.
  • await используется как сигнал для корутины, временно остановить выполнение и передать ресурсы назад в цикл, чтобы возобновить выполнение в последующем.

Другие особвенности: async для асинхронных генераторов + сокращения

Кроме стандартных применений async/await, Python позволяет использовать async, чтобы итерироваться через асинхронный итератор. Асинхронный итератор позволяет вызывать асинхронный код на каждой стадии итерирования.

Естественное продолжение этого концепта - асинхронный генератор. Для его повторного вызова вы можете использовать await, return или yield в натуральной корутине. Использовать yield в корутине стало возможно с Python 3.6 (PEP 525), в котором были представлены асинхронные генераторы, позволяющие использовать await и yield в теле одной функции- корутины:

-5

Кроме того, Python позволяет использовать асинхронное сокращение, с асинхронным циклом for. Как и и его синхронная тезка, это по большей части синтаксический сахар:

-6

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

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

Цикл событий и asyncio.run()

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

Весь менеджмент цикла событий запускается с помощью одной команды:

-7

asyncio.run() была представлена с python 3.7. Этот метод запускает цикл событий, отслеживает задачи до тех пор, пока все они не будут помечены как выполненные и закрывает цикл.

Есть более длинный способ управления циклом событий с помощью get_event_loop(). Обычно он выглядит примерно так:

-8

Скорее всего вы увидите примеры loop.get_event_loop() в более старых обзорах, но, если у вас нет особой необходимости вручную управлять циклом, asyncio.run() подойдет для большинства программ.

Если вам необходимо непосредственно взаимодействовать с циклом в Python программе, то знайте, что loop это старый добрый Python объект, который позволяет следить за собой с помощью loop.is_running() и loop.is_closed(). Вы можете использовать их, чтобы получить дополнительные возможности контроля. Например, можно упорядочивать обратную связь, обращаясь к циклу, как к аргументу.

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

#1: Корутины не представляют значения, если они не привязаны к циклу событий.

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

-9

Помните, что именно asyncio.run() запускает выполнение корутины, помещая её в цикл событий:

-10

(другие корутины могут быть исполнены с помощью await. Достаточно распространенный способ обернуть в asyncio.run() только main(), и вызывать связанные корутины из главной)

#2: По умолчанию цикл событий запускается в одном потоке на одном CPU. Обычно этого более чем достаточно. Вы так же можете запустить цикл событий на нескольких ядрах. Посмотрите выступление Джона Риза чтобы получить больше информации. Так же учтите, что ваш ноутбук вполне может загореться от таких нагрузок.

#3: Циклы событий подключаемы. Если вам действительно это необходимо, вы можете разработать свой. Отличный пример - пакет uvloop, который является реализацией цикла событий для Cython

Под "подключаемыми циклами событий" имеется ввиду, что вы можете использовать любую реализацию цикла событий, независимо от структур корутин. Сама библиотека asyncio уже содержит 2 реализации циклов. Стандартный построен на базе модуля selectors, а второй предназначен только для платформ на Windows.

__________________________________________________________________________________________

Супер, вы дочитали эту статью, это круто!) Ориентировочно осталось еще 1,5 таких материала. До встречи)

Гринч ревьюит мой код и похоже нашел что сказать.
Гринч ревьюит мой код и похоже нашел что сказать.

Наука
7 млн интересуются