Перейти к содержимому

Await python что это

  • автор:

Что такое аwaitable объект модуля asyncio в Python

Аwaitable объекты — это объекты, ожидающие результатов

В асинхронном программировании принято говорить, что объект является awaitable, то есть это объект, который, в какой-то момент времени может ничего полезного не делать, а заниматься только ожиданием каких-то результатов от сторонних сервисов (например ответа сервера с результатами на свой запрос). Такие объекты всегда запускаются с оператором await .

Практически все API-интерфейсы модуля asyncio предназначены для приема awaitable объектов — объектов ждущих каких-то результатов или команд на продолжение или приостановку работы от основного цикла событий.

Есть три основных типа объектов ожидания awaitable , которые можно запускать оператором await в асинхронном коде:

  • Сопрограмма coroutine — функция, определенная с оператором async .
  • Задача task — это сопрограмма, которая планируются для выполнения в будущем, как только это станет возможным.
  • Объекты будущих результатов futures — объект уже запущенной сопрограммы с еще неполученными/промежуточными результатами.

Сопрограммы/сoroutines.

Сопрограммы в Python являются объектами, которые могут ждать результатов от сторонних сервисов или своей очереди выполнения, следовательно, их можно также использовать с оператором wait внутри других сопрограмм:

import asyncio async def nested(): return 42 async def main(): # Ничего не происходит, если мы просто вызываем "nested()". # Сопрограмма `nested()` создается, но не будет выполняться, # т.к. в таком виде она заблокирует цикл событий, что недопустимо nested() # что бы асинхронная функция `nested()` заработала # необходимо заставить ее ждать своего выполнения # при помощи оператора `await` print(await nested()) asyncio.run(main()) # 42 

Важно. Здесь термин «сопрограмма» может использоваться для двух тесно связанных понятий:

  • функция сопрограммы: функция, определенная как *async def* ;
  • объект сопрограммы: объект, возвращаемый вызовом функции сопрограммы.

Модуль asyncio также поддерживает устаревшие сопрограммы на основе генератора. Поддержка сопрограмм на основе генераторов запланирована к удалению в Python 3.10.

Задачи Task .

Задачи ( asyncio.Task ) используются для одновременного планирования запуска нескольких сопрограмм. Когда сопрограмма оборачивается в задачу (передается в функцию asyncio.create_task() ), то сопрограмма будет автоматически запускаться в ближайшее время, как только будет это возможным:

import asyncio async def nested(): return 42 async def main(): # Запланируем запуск 'nested()' в одновременно с 'main()'. task = asyncio.create_task(nested()) # объект 'task' может теперь использоваться, для отмены # выполнения 'nested()' или ожидания ее выполнения: await task asyncio.run(main()) # 42 

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

Futures — объекты с будущими результатами.

Future объект — инкапсулирует асинхронное выполнение вызываемого объекта и представляет специальный низкоуровневый объект, который хранит промежуточное состояние запущенной задачи (когда она что-то ожидает) и в будущем, будет представлять конечный результат асинхронной операции. Этот объект может хранить информацию о том, что задача ещё не выполнена или не до конца выполнена, или может хранить уже полученный результат, или исключение, полученное во время выполнения кода.

Когда происходит ожидание объекта Future , это означает, что сопрограмма будет ждать, пока Future не будет разрешен в каком-то другом месте.

Futures объекты позволяют использовать код на основе обратного вызова (который сообщает о готовности объекта Future ) совместно с синтаксисом async/await , по этому они необходимы в асинхронном программировании.

Нет необходимости создавать объекты Future на уровне приложения. В основном, эти объекты создаются автоматически, при вызове функций или методов, предоставляемыми асинхронными API модулей:

async def main(): await function_that_returns_a_future_object() # это тоже правильно: await asyncio.gather( function_that_returns_a_future_object(), some_python_coroutine() ) 

Хорошим примером низкоуровневой функции, возвращающей Future объект, является функция loop.run_in_executor() .

  • КРАТКИЙ ОБЗОР МАТЕРИАЛА.
  • Сопрограммы и механизмы их запуска модулем asyncio
  • Что такое аwaitable объект модуля asyncio
  • Функция run() модуля asyncio
  • Менеджер контекста Runner() модуля asyncio
  • Функция create_task() модуля asyncio
  • Группы задач TaskGroup() модуля asyncio
  • Класс Task() модуля asyncio
  • Функция sleep() модуля asyncio
  • Функция gather() модуля asyncio
  • Функция shield() модуля asyncio
  • Асинхронный менеджер timeout() модуля asyncio
  • Асинхронный менеджер timeout_at() модуля asyncio
  • Функция wait_for() модуля asyncio
  • Функция as_completed() модуля asyncio
  • Функция wait() модуля asyncio
  • Функция to_thread() модуля asyncio
  • Функция run_coroutine_threadsafe() модуля asyncio
  • Функции current_task() и all_tasks() модуля asyncio
  • Использование очереди asyncio.Queue
  • Примитивы синхронизации задач в asyncio
  • Запуск внешних программ из кода asyncio
  • Работа с сетевыми соединениями модуля asyncio
  • Объект Future модуля asyncio Python и связанные функции
  • Создание и получение текущего цикла событий, модуль asyncio
  • Создание, запуск и остановка цикла событий модуля asyncio
  • Планирование обратных вызовов из цикла событий asyncio
  • Создание Future и Task из цикла событий asyncio
  • Немедленное выполнение задач модулем asyncio
  • Создание пулов потоков и процессов из цикла событий asyncio
  • Создание TCP, UDP и Unix соединений из цикла событий asyncio
  • Создание сетевых серверов из цикла событий asyncio
  • Создание субпроцесса из цикла событий asyncio
  • Работа с сокетами напрямую из цикла событий asyncio
  • Передача файлов из цикла событий asyncio
  • Наблюдение за дескрипторами файлов из цикла событий asyncio
  • DNS запросы из цикла событий asyncio
  • Сигналы Unix в циклах событий asyncio
  • Параллелизм и многопоточность в цикле событий asyncio
  • Объекты Transport и Protocol в цикле событий asyncio
  • Включение режима отладки в asyncio
  • Обработка исключений в цикле событий модуля asyncio
  • Исключения модуля asyncio Python

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

Акчурин И. С.

Отличия между асинхронным и синхронным кодом

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

Асинхронное программирование позволяет запускать операции параллельно, не дожидаясь выполнения последовательности. Это как если бы у вас было восемь рук и вы могли одновременно мыть посуду, пылесосить, читать газету и гладить кота. Жаль, что это невозможно в быту — зато вполне реально в разработке ПО. К тому же асинхронное программирование на Python становится все более популярным.

Где асинхронность применяется в реальном мире

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

Основы Python
Курс для IT-специалистов

Например, вы провожаете родственников и просите их позвонить, как только они доберутся до дома. А дальше спокойно занимаетесь своими делами и не названиваете им каждую минуту с проверкой: «Уже доехали?» Потому что звонок от родственников поступит сам, как только они доберутся до дома.

То есть функция «Звонок от родственников» ожидает события «Родственники приехали домой». И как только это событие произойдет, функция сразу сработает.

Или вы ставите напоминание в календаре на конкретную дату и время: «Созвон с коллегами». А потом напрочь забываете об этом событии. Но календарь не забудет!

Он обязательно напомнит об этом в указанное время. И ему плевать, что в этот момент вы можете мыться или стоять в трехчасовой пробке. Задача «Созвон с коллегами» находится в режиме ожидания. Она никак не мешает выполнению основной программы «Повседневная жизнь». Но как только наступит заранее заданное условие — дата и время, — эта функция активируется и вмешается в привычный ход жизни.

Особенности асинхронного программирования на Python

С чего начать изучение Python, как поймать Python-дзен и для чего этот язык пригодится в повседневной жизни, рассказываем на бесплатном вебинаре «Зачем специалисту Python?».

Обычные функции

Давайте запрограммируем такую ситуацию с уведомлением. Повседневную жизнь представим с помощью функции main ( ) . А уведомление — с помощью функции notification ( ) .

Внутри функции notification ( ) находится задержка в 10 секунд, чтобы имитировать отложенное задание. Если запустить код, то мы увидим, что поговорить с коллегой и поесть не удастся, потому что программа застынет на выполнении notification ( ) и будет ждать 10 секунд, пока уведомление не придет и не спасет от голодной смерти ��

Очевидно, что есть и разговаривать с коллегой мы хотим в то время, пока идет обратный отсчет до уведомления. То есть важно сделать так, чтобы работа функции notification ( ) не прерывала работу функции main ( ) .

Корутины

Чтобы решить этот вопрос, необходимо функцию notification ( ) сделать асинхронной, то есть работающей независимо от всего остального. Для этого в начале приписываем магическое async.

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

Что же произошло? async — это оператор Python, обертка над основной функцией. Теперь, когда вы вызовете функцию notification ( ) , она вернет специальный объект — корутину (coroutine).

  • Корутина тоже представляет собой функцию, которую нужно вызвать отдельно. Об этом и сообщает Python.

Обычный вызов notification ( ) теперь не исполняет функцию, а превращает ее в корутину, которую нужно вызвать. Чтобы это сделать, используем оператор await .

Теперь запуск кода приведет к новой проблеме:

Python будет выполнять await только в том случае, если он будет вызван внутри асинхронной функции. То есть функцию main ( ) необходимо тоже сделать асинхронной.

Но тогда мы получили еще одну корутину, которую также кто-то должен вызвать и сделать это опять только внутри асинхронной функции! Что же делать? Замкнутый круг ��

Asyncio спешит на помощь

Для управления асинхронными функциями в Python используется специальный модуль asyncio. Его поддержка впервые появилась в версии Python 3.5 в 2015 году. Добавим его к программе и запустим корутину main ( ) , используя метод run( ).

В терминах асинхронного программирования функция main ( ) будет называться циклом обработки событий (event loop). Это основной цикл программы, который вызывает все асинхронные функции, необходимые для работы.

Теперь программа вновь заработает! Правда, уведомление о созвоне снова не даст поесть и пообщаться с коллегой… Все опять работает как обычная последовательность действий.

Дело в том, что недостаточно просто запустить корутину — необходимо создать задачу (task), которая это сделает.

Ура! Наконец можно спокойно пообедать, поговорить с коллегой и заняться любыми другими делами. А уведомление о встрече придет тогда, когда истечет таймер, — в нашем случае это 10 секунд.

Запустите программу и обратите внимание на последовательность работы функции print( ). Несмотря на то что «Едим» и «Разговариваем с коллегой» выполняются после вызова корутины notification ( ) , мы увидим их первыми, потому что корутина выполняется асинхронно относительно основного цикла обработки событий main ( ) .

Знатоки Python могут закидать меня помидорами за такой пример, так как на самом деле time.sleep( ) нельзя использовать при создании нескольких асинхронных событий внутри event loop. А также не хватает вызова корутины через await . Но не торопитесь: обо всем по порядку.

Когда работы много

Зачем специалисту Python?
Бесплатный вебинар

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

Создадим основную функцию, внутри которой будем вызывать функции, возвращающие данные для погодной станции. Чтобы имитировать задержки, воспользуемся функцией time.sleep( ). Также будем измерять общее время
работы программы с помощью time.time( ).

Традиционное решение — вызов функций друг за другом. Так как выполнение main ( ) приостанавливается в момент вызова get_temp ( ) и get_pres ( ) , несложно подсчитать, что общее время работы будет равно сумме времени, затраченного на выполнение всех функций, вызываемых внутри main ( ) .

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

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

Почему так происходит? Все дело в задержках, которые мы создали. Асинхронно сейчас выполняется только main ( ) (цикл событий) относительно функций, которые он вызывает. Это видно по моментальному выводу о начале и конце измерения еще до того, как мы получили непосредственно сами данные.

А вот функции get_temp ( ) и get_pres ( ) выполняются последовательно, потому что внутри содержат обычную функцию time.sleep( ), тормозящую всю работу. Чтобы исправить этот эффект, необходимы корутины, которые будут создавать задержку и при этом не будут тормозить вызов других функций. Исправьте time.sleep( ) на asyncio.sleep( ) и не забудьте про await , ведь теперь мы работаем с корутинами.

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

Опять что-то не так… На самом деле все теперь работает корректно. Просто цикл событий main ( ) не дожидается окончания работы асинхронных функций и завершает свою работу раньше, чем должен. У нас же теперь все асинхронное, зачем ему кого-то ждать? ��

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

И время выполнения программы сократилось до 4 секунд вместо 6:

Теперь в цикл событий main ( ) можно добавить сколько угодно задач через asyncio. А общее время его работы всегда будет определяться временем работы самой долгой функции. Добро пожаловать в асинхронный мир!

Конкурентность и async / await¶

Здесь приведена подробная информация об использовании синтаксиса async def при написании функций обработки пути, а также рассмотрены основы асинхронного программирования, конкурентности и параллелизма.

Нет времени? ¶

TL;DR:

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

results = await some_library() 

В этом случае функции обработки пути необходимо объявлять с использованием синтаксиса async def :

@app.get('/') async def read_results():  results = await some_library() return results 

await можно использовать только внутри функций, объявленных с использованием async def .

Если вы обращаетесь к сторонней библиотеке, которая с чем-то взаимодействует (с базой данных, API, файловой системой и т. д.), и не имеет поддержки синтаксиса await (что относится сейчас к большинству библиотек для работы с базами данных), то объявляйте функции обработки пути обычным образом с помощью def , например:

@app.get('/') def results():  results = some_library() return results 

Если вашему приложению (странным образом) не нужно ни с чем взаимодействовать и, соответственно, ожидать ответа, используйте async def .

Если вы не уверены, используйте обычный синтаксис def .

Примечание: при необходимости можно смешивать def и async def в функциях обработки пути и использовать в каждом случае наиболее подходящий синтаксис. А FastAPI сделает с этим всё, что нужно.

В любом из описанных случаев FastAPI работает асинхронно и очень быстро.

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

Технические подробности¶

Современные версии Python поддерживают разработку так называемого «асинхронного кода» посредством написания «сопрограмм» с использованием синтаксиса async и await .

Ниже разберём эту фразу по частям:

Асинхронный код¶

Асинхронный код означает, что в языке �� есть возможность сообщить машине / программе ��, что в определённой точке кода ей �� нужно будет ожидать завершения выполнения чего-то ещё в другом месте. Допустим это что-то ещё называется «медленный файл» ��.

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

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

Потом он �� берёт первую выполненную задачу (допустим, наш «медленный файл» ��) и продолжает работу, производя с ней необходимые действия.

Вышеупомянутое «что-то ещё», завершения которого приходится ожидать, обычно относится к достаточно «медленным» операциям I/O (по сравнению со скоростью работы процессора и оперативной памяти), например:

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

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

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

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

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

Конкурентность и бургеры¶

Тот асинхронный код, о котором идёт речь выше, иногда называют «конкурентностью». Она отличается от «параллелизма».

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

Но внутреннее устройство конкурентности и параллелизма довольно разное.

Чтобы это понять, представьте такую картину:

Конкурентные бургеры¶

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

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

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

Кассир �� отдаёт вам чек с номером заказа.

В ожидании еды вы идёте со своей возлюбленной �� выбрать столик, садитесь и довольно продолжительное время общаетесь �� (поскольку ваши бургеры самые навороченные, готовятся они не так быстро ✨��✨).

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

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

И вот наконец настаёт этот момент, и вы идёте к стойке, чтобы забрать бургеры �� и вернуться за столик.

Вы со своей возлюбленной �� едите бургеры �� и отлично проводите время ✨.

А теперь представьте, что в этой небольшой истории вы компьютер / программа ��.

В очереди вы просто глазеете по сторонам ��, ждёте и ничего особо «продуктивного» не делаете. Но очередь движется довольно быстро, поскольку кассир �� только принимает заказы (а не занимается приготовлением еды), так что ничего страшного.

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

И хотя вы всё ещё не получили бургеры ��, ваша работа с кассиром �� ставится «на паузу» ⏸, поскольку теперь нужно ждать ��, когда заказ приготовят.

Но отойдя с номерком от прилавка, вы садитесь за столик и можете переключить �� внимание на свою возлюбленную �� и «работать» ⏯ �� уже над этим. И вот вы снова очень «продуктивны» ��, мило болтаете вдвоём и всё такое ��.

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

Поэтому вы подождёте, пока возлюбленная �� закончит рассказывать историю (закончите текущую работу ⏯ / задачу в обработке ��), и мило улыбнувшись, скажете, что идёте забирать заказ ⏸.

И вот вы подходите к стойке ��, к первоначальной задаче, которая уже завершена ⏯, берёте бургеры ��, говорите спасибо и относите заказ за столик. На этом заканчивается этап / задача взаимодействия с кассой ⏹. В свою очередь порождается задача «поедание бургеров» �� ⏯, но предыдущая («получение бургеров») завершена ⏹.

Параллельные бургеры¶

Теперь представим, что вместо бургерной «Конкурентные бургеры» вы решили сходить в «Параллельные бургеры».

И вот вы идёте со своей возлюбленной �� отведать параллельного фастфуда ��.

Вы становитесь в очередь пока несколько (пусть будет 8) кассиров, которые по совместительству ещё и повары ��‍����‍����‍����‍����‍����‍����‍����‍��, принимают заказы у посетителей перед вами.

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

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

Ни о чём не жалея, расплачиваетесь ��.

И кассир уходит на кухню ��‍��.

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

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

Это «синхронная» работа, вы «синхронизированы» с кассиром/поваром ��‍��. Приходится ждать �� у стойки, когда кассир/повар ��‍�� закончит делать бургеры �� и вручит вам заказ, иначе его случайно может забрать кто-то другой.

Наконец кассир/повар ��‍�� возвращается с бургерами �� после невыносимо долгого ожидания �� за стойкой.

Вы скорее забираете заказ �� и идёте с возлюбленной �� за столик.

Там вы просто едите эти бургеры, и на этом всё �� ⏹.

Вам не особо удалось пообщаться, потому что большую часть времени �� пришлось провести у кассы ��.

В описанном сценарии вы компьютер / программа �� с двумя исполнителями (вы и ваша возлюбленная ��), на протяжении долгого времени �� вы оба уделяете всё внимание ⏯ задаче «ждать на кассе».

В этом ресторане быстрого питания 8 исполнителей (кассиров/поваров) ��‍����‍����‍����‍����‍����‍����‍����‍��. Хотя в бургерной конкурентного типа было всего два (один кассир и один повар) �� ��‍��.

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

Так бы выглядел аналог истории про бургерную �� в «параллельном» мире.

Вот более реалистичный пример. Представьте себе банк.

До недавних пор в большинстве банков было несколько кассиров ��‍����‍����‍����‍�� и длинные очереди ����������������.

Каждый кассир обслуживал одного клиента, потом следующего ��‍��⏯.

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

Сомневаюсь, что у вас бы возникло желание прийти с возлюбленной �� в банк �� оплачивать налоги.

Выводы о бургерах¶

В нашей истории про поход в фастфуд за бургерами приходится много ждать ��, поэтому имеет смысл организовать конкурентную систему ⏸��⏯.

И то же самое с большинством веб-приложений.

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

Потом снова ждать ��, пока вернётся ответ.

Это ожидание �� измеряется микросекундами, но если всё сложить, то набегает довольно много времени.

Вот почему есть смысл использовать асинхронное ⏸��⏯ программирование при построении веб-API.

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

При том, что основная спецификация асинхронного взаимодействия Python с веб-сервером (ASGI) была разработана командой Django для внедрения поддержки веб-сокетов.

Именно асинхронность сделала NodeJS таким популярным (несмотря на то, что он не параллельный), и в этом преимущество Go как языка программирования.

И тот же уровень производительности даёт FastAPI.

Поскольку можно использовать преимущества параллелизма и асинхронности вместе, вы получаете производительность лучше, чем у большинства протестированных NodeJS фреймворков и на уровне с Go, который является компилируемым языком близким к C (всё благодаря Starlette).

Получается, конкурентность лучше параллелизма?¶

Нет! Мораль истории совсем не в этом.

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

Давайте посмотрим с другой стороны, представьте такую картину:

Вам нужно убраться в большом грязном доме.

Да, это вся история.

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

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

И понадобится одинаковое количество времени с очередью (конкурентностью) и без неё, и работы будет сделано тоже одинаковое количество.

Однако в случае, если бы вы могли привести 8 бывших кассиров/поваров, а ныне уборщиков ��‍����‍����‍����‍����‍����‍����‍����‍��, и каждый из них (вместе с вами) взялся бы за свой участок дома, с такой помощью вы бы закончили намного быстрее, делая всю работу параллельно.

В описанном сценарии каждый уборщик (включая вас) был бы исполнителем, занятым на своём участке работы.

И поскольку большую часть времени выполнения занимает реальная работа (а не ожидание), а работу в компьютере делает ЦП , такие задачи называют ограниченными производительностью процессора .

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

  • Обработка звука или изображений.
  • Компьютерное зрение: изображение состоит из миллионов пикселей, в каждом пикселе 3 составляющих цвета, обработка обычно требует проведения расчётов по всем пикселям сразу.
  • Машинное обучение: здесь обычно требуется умножение «матриц» и «векторов». Представьте гигантскую таблицу с числами в Экселе, и все их надо одновременно перемножить.
  • Глубокое обучение: это область машинного обучения, поэтому сюда подходит то же описание. Просто у вас будет не одна таблица в Экселе, а множество. В ряде случаев используется специальный процессор для создания и / или использования построенных таким образом моделей.

Конкурентность + параллелизм: Веб + машинное обучение¶

FastAPI предоставляет возможности конкуретного программирования, которое очень распространено в веб-разработке (именно этим славится NodeJS).

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

Необходимо также отметить, что Python является главным языком в области дата-сайенс , машинного обучения и, особенно, глубокого обучения. Всё это делает FastAPI отличным вариантом (среди многих других) для разработки веб-API и приложений в области дата-сайенс / машинного обучения.

Как добиться такого параллелизма в эксплуатации описано в разделе Развёртывание.

async и await ¶

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

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

burgers = await get_burgers(2) 

Главное здесь слово await . Оно сообщает интерпретатору, что необходимо дождаться ⏸ пока get_burgers(2) закончит свои дела ��, и только после этого сохранить результат в burgers . Зная это, Python может пока переключиться на выполнение других задач �� ⏯ (например получение следующего запроса).

Чтобы ключевое слово await сработало, оно должно находиться внутри функции, которая поддерживает асинхронность. Для этого вам просто нужно объявить её как async def :

async def get_burgers(number: int):  # Готовим бургеры по специальному асинхронному рецепту return burgers 
# Это не асинхронный код def get_sequential_burgers(number: int):  # Готовим бургеры последовательно по шагам return burgers 

Объявление async def указывает интерпретатору, что внутри этой функции следует ожидать выражений await , и что можно поставить выполнение такой функции на «паузу» ⏸ и переключиться на другие задачи ��, с тем чтобы вернуться сюда позже.

Если вы хотите вызвать функцию с async def , вам нужно «ожидать» её. Поэтому такое не сработает:

# Это не заработает, поскольку get_burgers объявлена с использованием async def burgers = get_burgers(2) 

Если сторонняя библиотека требует вызывать её с ключевым словом await , необходимо писать функции обработки пути с использованием async def , например:

@app.get('/burgers') async def read_burgers():  burgers = await get_burgers(2)  return burgers 

Технические подробности¶

Как вы могли заметить, await может применяться только в функциях, объявленных с использованием async def .

Но выполнение такой функции необходимо «ожидать» с помощью await . Это означает, что её можно вызвать только из другой функции, которая тоже объявлена с async def .

Но как же тогда появилась первая курица ? В смысле. как нам вызвать первую асинхронную функцию?

При работе с FastAPI просто не думайте об этом, потому что «первой» функцией является ваша функция обработки пути, и дальше с этим разберётся FastAPI.

Кроме того, если хотите, вы можете использовать синтаксис async / await и без FastAPI.

Пишите свой асинхронный код¶

Starlette (и FastAPI) основаны на AnyIO, что делает их совместимыми как со стандартной библиотекой asyncio в Python, так и с Trio.

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

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

Другие виды асинхронного программирования¶

Стиль написания кода с async и await появился в языке Python относительно недавно.

Но он сильно облегчает работу с асинхронным кодом.

Ровно такой же синтаксис (ну или почти такой же) недавно был включён в современные версии JavaScript (в браузере и NodeJS).

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

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

Что касается JavaScript (в браузере и NodeJS), раньше там использовали для этой цели «обратные вызовы» . Что выливалось в ад обратных вызовов.

Сопрограммы¶

Корути́на (или же сопрограмма) — это крутое словечко для именования той сущности, которую возвращает функция async def . Python знает, что её можно запустить, как и обычную функцию, но кроме того сопрограмму можно поставить на паузу ⏸ в том месте, где встретится слово await .

Всю функциональность асинхронного программирования с использованием async и await часто обобщают словом «корутины». Они аналогичны «горутинам» , ключевой особенности языка Go.

Заключение¶

В самом начале была такая фраза:

Современные версии Python поддерживают разработку так называемого «асинхронного кода» посредством написания «сопрограмм» с использованием синтаксиса async и await .

Теперь всё должно звучать понятнее. ✨

На этом основана работа FastAPI (посредством Starlette), и именно это обеспечивает его высокую производительность.

Очень технические подробности¶

Этот раздел читать не обязательно.

Здесь приводятся подробности внутреннего устройства FastAPI.

Но если вы обладаете техническими знаниями (корутины, потоки, блокировка и т. д.) и вам интересно, как FastAPI обрабатывает async def в отличие от обычных def , читайте дальше.

Функции обработки пути¶

Когда вы объявляете функцию обработки пути обычным образом с ключевым словом def вместо async def , FastAPI ожидает её выполнения, запустив функцию во внешнем пуле потоков , а не напрямую (это бы заблокировало сервер).

Если ранее вы использовали другой асинхронный фреймворк, который работает иначе, и привыкли объявлять простые вычислительные функции через def ради незначительного прироста скорости (порядка 100 наносекунд), обратите внимание, что с FastAPI вы получите противоположный эффект. В таком случае больше подходит async def , если только функция обработки пути не использует код, приводящий к блокировке I/O .

Но в любом случае велика вероятность, что FastAPI окажется быстрее другого фреймворка (или хотя бы на уровне с ним).

Зависимости¶

То же относится к зависимостям. Если это обычная функция def , а не async def , она запускается во внешнем пуле потоков.

Подзависимости¶

Вы можете объявить множество ссылающихся друг на друга зависимостей и подзависимостей (в виде параметров при определении функции). Какие-то будут созданы с помощью async def , другие обычным образом через def , и такая схема вполне работоспособна. Функции, объявленные с помощью def будут запускаться на внешнем потоке (из пула), а не с помощью await .

Другие служебные функции¶

Любые другие служебные функции, которые вы вызываете напрямую, можно объявлять с использованием def или async def . FastAPI не будет влиять на то, как вы их запускаете.

Этим они отличаются от функций, которые FastAPI вызывает самостоятельно: функции обработки пути и зависимости.

Если служебная функция объявлена с помощью def , она будет вызвана напрямую (как вы и написали в коде), а не в отдельном потоке. Если же она объявлена с помощью async def , её вызов должен осуществляться с ожиданием через await .

Ещё раз повторим, что все эти технические подробности полезны, только если вы специально их искали.

В противном случае просто ознакомьтесь с основными принципами в разделе выше: Нет времени?.

Асинхронный Python: различные формы конкурентности

Асинхронный Python: различные формы конкурентности

Это перевод статьи Абу Ашраф Маснуна «Async Python: The Different Forms of Concurrency».

С появлением Python 3 довольно много шума об «асинхронности» и «параллелизме», можно полагать, что Python недавно представил эти возможности/концепции. Но это не так. Мы много раз использовали эти операции. Кроме того, новички могут подумать, что asyncio является единственным или лучшим способом воссоздать и использовать асинхронные/параллельные операции. В этой статье мы рассмотрим различные способы достижения параллелизма, их преимущества и недостатки.

Определение терминов:

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

Синхронный и асинхронный:

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

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

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

Конкурентность и параллелизм:

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

Когда мы попросили позвонить друга, а сами писали письмо, то задачи выполнялись ​параллельно.​

Параллелизм по сути является формой конкурентности. Но параллелизм зависит от оборудования. Например, если в CPU только одно ядро, то две задачи не могут выполняться параллельно. Они просто делят процессорное время между собой. Тогда это конкурентность, но не параллелизм. Но когда у нас есть несколько ядер, мы можем выполнять несколько операций (в зависимости от количества ядер) одновременно.

  • Синхронность: блокирует операции (блокирующие)
  • Асинхронность: не блокирует операции (неблокирующие)
  • Конкурентность: совместный прогресс (совместные)
  • Параллелизм: параллельный прогресс (параллельные)

Параллелизм подразумевает конкурентность. Но конкурентность не всегда подразумевает параллелизм.

Потоки и процессы

Python поддерживает потоки уже очень давно. Потоки позволяют выполнять операции конкурентно. Но есть проблема, связанная с Global Interpreter Lock (GIL) из-за которой потоки не могли обеспечить настоящий параллелизм. И тем не менее, с появлением multiprocessing можно использовать несколько ядер с помощью Python.

Потоки (Threads)

Рассмотрим небольшой пример. В нижеследующем коде функция worker будет выполняться в нескольких потоках асинхронно и одновременно.

import threading import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker <>, I slept for <> seconds".format(number, sleep)) for i in range(5): t = threading.Thread(target=worker, args=(i,)) t.start() print("All Threads are queued, let's see when they finish!")

А вот пример выходных данных:

$ python thread_test.py All Threads are queued, let's see when they finish! I am Worker 1, I slept for 1 seconds I am Worker 3, I slept for 4 seconds I am Worker 4, I slept for 5 seconds I am Worker 2, I slept for 7 seconds I am Worker 0, I slept for 9 seconds

Таким образом мы запустили 5 потоков для совместной работы, и после их старта (т.е. после запуска функции worker) операция не ждёт завершения работы потоков прежде чем перейти к следующему оператору print. Это асинхронная операция.

В нашем примере мы передали функцию в конструктор Thread. Если бы мы хотели, то могли бы реализовать подкласс с методом (ООП стиль).

Global Interpreter Lock (GIL)

GIL нужен, чтобы сделать обработку памяти CPython проще и обеспечить наилучшую интеграцию с C.

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

Краткие сведения о GIL:

  • Одновременно может выполняться один поток.
  • Интерпретатор Python переключается между потоками для достижения конкурентности.
  • GIL применим к CPython (стандартной реализации). Но, например, Jython и IronPython не имеют GIL.
  • GIL делает однопоточные программы быстрыми.
  • Операциям ввода/вывода GIL обычно не мешает.
  • GIL позволяет легко интегрировать непотокобезопасные библиотеки на C, благодаря GIL у нас есть много высокопроизводительных расширений/модулей, написанных на C.
  • Для CPU-зависимых задач интерпретатор делает проверку каждые N тиков и переключает потоки. Таким образом один поток не блокирует другие.

Многие видят в GIL слабость. Я же рассматриваю это как благо, ведь были созданы такие библиотеки, как NumPy, SciPy, которые занимают особое, уникальное положение в научном обществе.

Процессы (Processes)

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

Давайте просто пойдем и изменим предыдущий пример. Теперь модифицированная версия использует Процесс вместо Потока.

import multiprocessing import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker <>, I slept for <> seconds".format(number, sleep)) for i in range(5): t = multiprocessing.Process(target=worker, args=(i,)) t.start() print("All Processes are queued, let's see when they finish!")

Что же изменилось? Я просто импортировал модуль multiprocessing вместо threading. А затем, вместо потока я использовал процесс. Вот и всё! Теперь вместо множества потоков мы используем процессы, которые запускаются на разных ядрах CPU (если, конечно, у вашего процессора несколько ядер).

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

Пример из официальных документов:

from multiprocessing import Pool def f(x): return x*x if __name__ == '__main__': p = Pool(5) print(p.map(f, [1, 2, 3]))

Здесь вместо того, чтобы перебирать список значений и вызывать функцию f по одному, мы фактически запускаем функцию в разных процессах.

Один процесс выполняет f(1), другой-f(2), а другой-f (3). Наконец, результаты снова объединяются в список. Это позволяет нам разбить тяжелые вычисления на более мелкие части и запускать их параллельно для более быстрого расчета.

Модуль concurrent.futures

Модуль concurrent.futures большой и позволяет писать асинхронный код очень легко. Мои любимчики — ThreadPoolExecutor и ProcessPoolExecutor. Эти исполнители поддерживают пул потоков или процессов. Мы отправляем наши задачи в пул, и он запускает задачи в доступном потоке / процессе. Возвращается объект Future, который можно использовать для запроса и получения результата по завершении задачи.

А вот пример ThreadPoolExecutor:

from concurrent.futures import ThreadPoolExecutor from time import sleep def return_after_5_secs(message): sleep(5) return message pool = ThreadPoolExecutor(3) future = pool.submit(return_after_5_secs, ("hello")) print(future.done()) sleep(5) print(future.done()) print(future.result())

Asyncio — что, как и почему

У вас, вероятно, есть вопрос, который есть у многих людей в сообществе Python — что asyncio приносит нового? Зачем нужен был еще один способ асинхронного ввода-вывода? Разве у нас уже не было потоков и процессов?

Зачем нам нужен asyncio?

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

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

Назовем потоки — T1, T2 и T3. Три потока начали свою операцию ввода-вывода. T3 завершает его первым. T2 и T1 все еще ожидают ввода-вывода. Интерпретатор Python переключается на T1, но он все еще ждет. Хорошо, интерпретатор перемещается в T2, а тот все еще ждет, а затем перемещается в T3, который готов и выполняет код. Вы видите в этом проблему?

T3 был готов, но интерпретатор сначала переключился между T2 и T1 — это понесло расходы на переключение, которых мы могли бы избежать, если бы интерпретатор сначала переключился на T3, верно?

Что есть asynio?

Asyncio предоставляет нам цикл событий наряду с другими крутыми вещами. Цикл событий (event loop) отслеживает события ввода/вывода и переключает задачи, которые готовы и ждут операции ввода/вывода.

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

Как использовать asyncio?

Прежде чем мы начнём, давайте взглянем на пример:

import asyncio import datetime import random async def my_sleep_func(): await asyncio.sleep(random.randint(0, 5)) async def display_date(num, loop): end_time = loop.time() + 50.0 while True: print("Loop: <> Time: <>".format(num, datetime.datetime.now())) if (loop.time() + 1.0) >= end_time: break await my_sleep_func() loop = asyncio.get_event_loop() asyncio.ensure_future(display_date(1, loop)) asyncio.ensure_future(display_date(2, loop)) loop.run_forever()

Обратите внимание, что синтаксис async/await предназначен только для Python 3.5 и выше. Пройдёмся по коду:

  • У нас есть асинхронная функция display_date, которая принимает число (в качестве идентификатора) и цикл обработки событий в качестве параметров.
  • Функция имеет бесконечный цикл, который прерывается через 50 секунд. Но за этот период она неоднократно печатает время и делает паузу. Функция await может ожидать завершения выполнения других асинхронных функций (корутин).
  • Передаем функцию в цикл обработки событий (используя метод ensure_future).
  • Запускаем цикл событий.

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

Делаем правильный выбор

Только что мы прошлись по самым популярным формам конкурентности. Но остаётся вопрос — что следует выбрать?

Это зависит от вариантов использования. Из моего опыта я склонен следовать этому псевдо-коду:

if io_bound: if io_very_slow: print("Use Asyncio") else: print("Use Threads") else: print("Multi Processing")

Такие сложные материи, как асинхронность, мы проходим на обучении Рython ��

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *