Как запустить асинхронную функцию python
Перейти к содержимому

Как запустить асинхронную функцию python

  • автор:

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

Ассинхроность дает нам плюс к многозадачности.Даваете посмотрим два примера кода:

import time def start(): print(1) def time(): time.sleep(20) print(2) def end(): print(3) def main(): start_time = time.time() start() time() end() end_time = time.time() elapsed_time = end_time — start_time print(f»Elapsed time: seconds») if __name__ == «__main__»: main()

И с ассинхроностью:

import asyncio import time async def start(): print(1) async def time(): await asyncio.sleep(20) print(2) def end(): print(3) async def main(): start_time = time.time() await start() await time() end() end_time = time.time() elapsed_time = end_time — start_time print(f»Elapsed time: seconds») if __name__ == «__main__»: asyncio.run(main())

Здесь скорость двух примеров относительна близка,но в больших проектах лучше использовать ассинхроность.

Теперь переидем к обьяснению:

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

import asyncio async def task1(): print(«Task 1 started») await asyncio.sleep(2) # Имитируем длительную операцию print(«Task 1 completed») async def task2(): print(«Task 2 started») await asyncio.sleep(1) # Имитируем длительную операцию print(«Task 2 completed») async def main(): print(«Main started») await asyncio.gather(task1(), task2()) # Запускаем задачи параллельно print(«Main completed») asyncio.run(main())

В этом примере мы используем модуль asyncio для создания асинхронной программы. Есть две асинхронные задачи task1() и task2(), которые имитируют длительные операции, используя await asyncio.sleep() для приостановки выполнения на определенное время.

Функция main() является точкой входа в программу и запускает задачи task1() и task2() параллельно с помощью asyncio.gather(). Важно отметить, что когда одна из задач ожидает (await), другая задача может продолжить выполнение, не блокируя программу.

Выполнение кода показывает, что «Task 1 started» и «Task 2 started» выводятся одновременно, а затем после задержки «Task 2 completed» выводится раньше, чем «Task 1 completed». Это демонстрирует параллельное выполнение асинхронных задач.

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

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

Спасибо за просмотр статьй.

Корутины в Python

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

Давайте сразу рассмотрим пример асинхронной функции:

import asyncio async def count_to_three(): print("Веду отсчёт. 1") await asyncio.sleep(0) print("Веду отсчёт. 2") await asyncio.sleep(0) print("Веду отсчёт. 3") await asyncio.sleep(0) 

Очень похоже на обычную функцию, однако здесь есть два новых слова: async и await .

async говорит Питону о том, что мы пишем не просто функцию, а асинхронную функцию. Просто добавили async и всё, функция теперь асинхронная.

Второе слово — await. Оно прерывает исполнение функции, и возвращает управление программой наружу. После этого корутину можно запустить повторно, а затем еще и еще, и каждый раз она будет продолжать работу с того await , на котором прервалась ранее. Например, в функции count_to_three команда await встречается три раза, значит корутину можно вызвать четыре раза (да, не три!). Корутина будет работать до первого await, затем до второго, до третьего и на четвёртый раз выполнит остатки до конца.

Нельзя делать await None или await «Hello, World!» . Можно await только то, что так и называют — «awaitable».

await asyncio.sleep(0) — это команда корутине «Дай поработать другим!»

Сразу покажем, как это выглядит на практике:

coroutine_counter = count_to_three() print(coroutine_counter) # coroutine_counter.send(None) # Выведет "Веду отсчёт. 1" coroutine_counter.send(None) # Выведет "Веду отсчёт. 2" coroutine_counter.send(None) # Выведет "Веду отсчёт. 3" 

Мы вызываем асинхронную функцию count_to_three , однако она не выводит на экран цифру 1, а возвращает корутину. Все асинхронные функции так делают. Это сделано для того, чтобы у вас был объект этой корутины в переменной. Теперь корутину можно запускать раз за разом, а она раз за разом будет делать кусочек и останавливаться на следующем await .

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

Нельзя просто .send() . Всегда нужно передавать какое-то значение. Об этом тоже расскажем позже. Пока что воспринимайте .send(None) как команду «продолжи выполнять корутину».

Когда корутина закончится?

Она остановится навсегда, когда закончатся все await или встретится return . Когда корутина заканчивается — она истощается и вызов .send() выдаёт ошибку:

coroutine_counter = count_to_three() coroutine_counter.send(None) # Выведет "Веду отсчёт. 1" coroutine_counter.send(None) # Выведет "Веду отсчёт. 2" coroutine_counter.send(None) # Выведет "Веду отсчёт. 3" coroutine_counter.send(None) # Выбросит ошибку StopIteration 

Если мы хотим запустить наш счётчик сначала, придётся создать новую корутину, вызвав count_to_three() :

coroutine_counter = count_to_three() coroutine_counter.send(None) # Выведет "Веду отсчёт. 1" coroutine_counter.send(None) # Выведет "Веду отсчёт. 2" coroutine_counter_new = count_to_three() coroutine_counter_new.send(None) # Снова выведет "Веду отсчёт. 1", новая корутина 

Обычно заранее не известно сколько await будет до момента «истощения», поэтому исключение приходится «перехватывать»:

coroutine_counter = count_to_three() while True: try: coroutine_counter.send(None) # В четвёртый раз здесь вылетит StopIteration except StopIteration: break 

Исключение StopIteration возникает всего один раз. Если после него попробовать запустить корутину ещё раз, то поднимется другое исключение — RuntimeError , и оно уже будет считаться ошибкой. О том как работать с исключениями читайте в статье про try except.

Нельзя запускать истощённую корутину.

Добиваемся асинхронности

С корутинами разобрались, останавливать их научились. А зачем.

Корутины позволят вашему коду работать асинхронно, т.е. делать несколько вещей одновременно. Допустим, вы решили скачать несколько файлов. Обычный, синхронный код скачивает файлы по-очереди. Сначала первый файл целиком, затем второй, тоже целиком. Асинхронный код качает файлы одновременно, по кусочкам. Приведём пример скачивания двух файлов:

async def download_file(url): # здесь происходит какая-то логика со скачиванием файла image_downloader = download_file('https://www.some-images.com/image1.jpg') music_downloader = download_file('https://www.music-site.com/artist/album/song5.mp3') coroutines = [music_downloader, image_downloader] while True: for coroutine in coroutines.copy(): try: coroutine.send(None) except StopIteration: coroutines.remove(coroutine) if len(coroutines) == 0: break 

Разберём как работает код:

  1. Мы создали 2 корутины: image_downloader и music_downloader . Первая качает картинку по ссылке https://www.some-images.com/image1.jpg , вторая — музыку по ссыке https://www.music-site.com/artist/album/song5.mp3 .
  2. Мы положили их в список coroutines
  3. В бесконечном цикле мы по очереди запускаем все корутины из списка. Если вышла ошибка StopIteration — корутина истощилась, т.е. файл скачан. Убираем её из списка, корутина больше запускаться не будет.
  4. Чтобы итерация по списку coroutines не сбивалась после удаления элемента из него итерируем не по оригиналу, а по копии coroutines.copy() .
  5. Если список с корутинами закончился (его длина равна нулю), пора заканчивать и бесконечный цикл, потому что все файлы скачаны.

Передать параметры в асинхронную функцию

В плане аргументов асинхронные функции ничем не отличаются от обычных. Доработаем пример со счетчиком и вместо async def count_to_three напишем универсальную функцию async def count :

import asyncio async def count(limit=3): for step in range(1, limit+1): print("Веду отсчёт.", step) await asyncio.sleep(0) coroutine = count(5) while True: coroutine.send(None) 
Веду отсчёт. 1 Веду отсчёт. 2 Веду отсчёт. 3 Веду отсчёт. 4 Веду отсчёт. 5 Traceback (most recent call last): File "", line 2, in StopIteration 

Попробуйте бесплатные уроки по Python

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

Переходите на страницу учебных модулей «Девмана» и выбирайте тему.

Асинхронность python на примере

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

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

 
import time

def waiter():
cook('Паста', 8)
cook('Салат Цезарь', 3)
cook('Отбивные', 16)

def cook(order, time_to_prepare):
print(f'Новый заказ: ')
time.sleep(time_to_prepare)
print(order, '- готово')

if __name__ == '__main__':
waiter()

Сохраним файл sync.py .

Здесь повар симулируется в виде функции. Он принимает заказ и время на его приготовление. Затем с помощью функции time.sleep симулируется сам процесс готовки. А по завершении выводится сообщение о том, что заказ готов.

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

Убедитесь, что установлена версия Python 3.7+ с помощью команды python3 --version на Mac или python --version — на Windows. Если версия меньше 3.7, обновите.

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

Новый заказ: Паста Паста - готово Новый заказ: Салат Цезарь Салат Цезарь - готово Новый заказ: Отбивные Отбивные - готово

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

Теперь конвертируем программу так, чтобы она использовала библиотеку asyncio. Это будет первый шаг для того, чтобы разобраться с тем, как писать асинхронный код. Скопируем файл sync.py в новый файл coros.py со следующим кодом:

 
import asyncio
import time

async def waiter() -> None:
cook('Паста', 8)
cook('Салат Цезарь', 3)
cook('Отбивные', 16)

async def cook(order: str, time_to_prepare: int) -> None:
print(f'Новый заказ: ')
time.sleep(time_to_prepare)
print(order, '- готово')

asyncio.run(waiter())

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

В конце программы заменим if __name__ == '__main__' на новый метод run из модуля asyncio . Что именно делает run ?

По сути, run берет низкоуровневый псевдо-сервер asyncio, который называется рабочим циклом. Этот цикл является координатором, который следит за приостановкой и возобновлением задач из кода. В примере с поваром и официантом вызов «cook(»Паста’)» — это задача, которая выполнится, но также будет приостановлена на 8 секунд. Таким образом после получения запроса он отмечается, а программа переходит к выполнению следующего. После завершения заказа на приготовление пасты цикл продолжит выполнение на следующей строке, где готовится салат «Цезарь».

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

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

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

 
async def waiter() -> None:
await cook('Паста', 8)
await cook('Салат Цезарь', 3)
await cook('Отбивные', 16)

Функция waiter объявляется асинхронной за счет добавления приставки async в начале. После этого появляется возможность сообщать asyncio, какие из задач будут асинхронными внутри. Для этого к ним добавляется ключевое слово await .

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

Достаточно лишь запомнить, что если есть задачи с await , то сама функция должна быть объявлена с async .

А как же функция cook ? Ее тоже нужно сделать асинхронной, поэтому перепишем ее вот так.

 
async def cook(order, time_to_prepare):
print(f'Новый заказ: ')
await time.sleep(time_to_prepare)
print(order, '- готово')

Но здесь есть одна проблема. Если использовать стандартную функцию time.sleep , то она заблокирует весь процесс выполнения, сделав асинхронную программу бесполезной. В этом случае нужно использовать функцию sleep из модуля asyncio.

 
async def cook(order, time_to_prepare):
print(f'Новый заказ: ')
await asyncio.sleep(time_to_prepare)
print(order, '- готово')

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

Если сейчас запустить программу, то результат будет таким:

Новый заказ: Паста Паста - готово Новый заказ: Салат Цезарь Салат Цезарь - готово Новый заказ: Отбивные Отбивные - готово

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

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

Сопрограммы и задачи (coroutines and tasks)

Сопрограммы (coroutines)

Функции waiter и cook трансформируются именно в тот момент, когда перед их определением ставится ключевое слово async . С этого момент их можно считать сопрограммами.

Если попытаться запустить одну из таких прямо, то вернется сообщение с информацией о ней, но сама программа не будет запущена. Попробуем запустить терминал Python и импортировать туда функцию cook из файла coros . Во-первых, нужно закомментировать команду asyncio.run так, чтобы код не выполнялся. После этого файл можно сохранить.

# asyncio.run(waiter())

Затем откроем терминал и сделаем следующее:

Как вызвать асинхронную функцию?

При простом вызове get_app() я получаю генератор, если использовать await , то выдаёт ошибку SyntaxError: 'await' outside function , если делать вызов через asyncio.run(get_app()) выдаёт ошибку RuntimeError: asyncio.run() cannot be called from a running event loop . p.s. хочу вернуть значение app_info

Отслеживать
задан 20 окт 2020 в 21:12
23 1 1 серебряный знак 3 3 бронзовых знака

1 ответ 1

Сортировка: Сброс на вариант по умолчанию

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

Из колбэка получить результат можно только в новый колбэк. привет яваскрипт 😉

def second_cb(fut): app_info = fut.result() продолжение кода def server_cb(request): asyncio.ensure_future(get_app()).add_done_callback(second_cb) return 

second_cb выполнится после завершения server_cb.

Чтоб избежать callback-hell перевызывай server_cb как асинхронную функцию и делай там await-ы

def server_cb(request): async def server_cb_async(request): r = await get_app() r2 = await another(r) # вся обработка вложенна тут. asyncio.ensure_future(server_cb_async(request)).add_done_callback(lambda x: pass) 

или запустить асинхронный вариант как таск

 asyncio.get_running_loop().create_task(server_cb_async(request)) 

В последнее время я через таски делаю.

Как альтернативный вариант можно использовать готовую джанговскую async-to-sync

from asgiref.sync import async_to_sync res = async_to_sync(get_app()) 
@async_to_sync async def get_app(. ): 

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

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