Pull to refresh

Comments 56

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

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

для большинства штук асинхронных библиотек нет

Для какого большинства и каких штук?

Есть огромное кол-во библиотек на 99% кейсов с I/O с которыми можно столкнуться в повседневной работе (БД, htpp клиент/сервер, сокеты)

Чего ещё не хватает web-разработчику для счастья?

А ещё иногда бывает не только веб... 🤷‍♀️

В Linux доступ к файловой системе по определению не асинхронный

Уже нет. Недавно эта проблема была решена с помощью io_uring.

Там есть ещё древнючий aio (aka POSIX asynchronous I/O) - в питоне через caio / aiofile.

В Linux доступ к файловой системе по определению не асинхронный,

В линуксе много разных методов. Вы говорите про posix, который Линукс (большей частью) поддерживает. Про io_uring выше комментарий.

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

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

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

А так, статья очень полезная. Спасибо большое, жду продолжение!

Вот именно этого я и добивался) Не запуская код невозможно чему-то научиться. Удачи!

Отличная статья! Прочёл с удовольствием!

зануда_mode = on

Пример 2.3

используются три функции библиотеки asyncio вместо одной run в предыдущем примере.

в примере 2.2 используются asyncio.create_task, asyncio.run, asyncio.sleep. Вероятно, подразумевалось, что все убрано в функции и в главной функции вызывается только одна функция asyncio.

зануда_mode = off

вопрос по тексту, можно ли в Пример 5.2?

async def main(cities_):
  for city in cities_:
    await asyncio.create_task(get_weather(city))

и если да, то можно ли:

async def main(cities_):
  yield from (await asyncio.create_task(get_weather(city)) for city in cities)

но, поскольку, возвращаем "Task" зачем тогда async в main

def main(cities_):
  yield from (asyncio.create_task(get_weather(city)) for city in cities)

Или я что-то упустил?

зануда_mode = onКод приведён в тексте, можно и попробовать самому.зануда_mode = off

Первое выполнится синхронно. Второе и третье не выполнится "yield from" not allowed in an async function, "AsyncGenerator[None, None]" is not iterable

попробовал сам. То, что я хотел, не работает без gather

async def main(cities_):
    await asyncio.gather(*(asyncio.create_task(get_weather(city)) for city in cities_))

жаль, что gather не жрет нераспакованные генераторы.

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

А после запуска примера 2.3 было принято волевое решение дальше не читать (очень жаль):

У вас какая версия python? Попробуйте 3.10

Python 3.9.7

Но это на собственном ноуте, а в проде везде не выше3.6, поэтому, к сожалению, статья про версию 3.10+ для меня мало актуальна) спасибо за ответ

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

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

asyncio.run появилась в python 3.7 (см. docs.python.org/3/library/asyncio-task.html#asyncio.run)
До этого нужно было писать
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()


Другая возможная проблема — это использование IPython ⩾ 7.0 или соответствующего Jupyter. Там уже запущен свой event loop от ipython'а и нельзя создать новый.
Соответственно нужно вызывать первую функцию сразу с await'ом
await main()

А чтобы получить текущий event loop —
loop = asyncio.get_running_loop()

Спасибо, в первом примере помог await main(), во втором -

import nest_asyncio
nest_asyncio.apply()

@EvilsInterrupt

К сожалению да, Windows :( set_event_loop_policy не помогло

Это не вина автора вы используете Windows , а на эту тему есть общеизвестный баг . Попробуйте применить: asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()

Anaconda (Spyder) сама по себе является асинхронной программой на Питоне. Поэтому внутри неё не вызвать ещё более асинхронный код. О чем и говорит ошибка: cannot be called from running event loop.

А вы случайно не в jupyter notebook его запускали?

конечно в юпитере)

если бы не юпитер, я бы может никогда и программировать не стал)

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

Автор, вот встретились бы лично - пожал бы руку!!! Только недавно с этим асинх боролся и тут такой подарок! Лучи добра тебе!:)

Суперская статья! Действительно самое простое объяснение из тех что я видел

Не понял логики разработчиков библиотеки: зачем нужно каждую функцию отправлять в asyncio.create_task, ведь функция и так объявлена как asinc - увидели, что запускаем asinc -функцию, и выполняйте create_task и всё, что нужно, но под капотом…?

async def my_breakfast():

print('поставил варить яйцо')

await asyncio.sleep(300)

print('съел яйцо')

#

async def your_breakfast():

print('поставил варить яйцо')

await asyncio.sleep(300)

print('съел яйцо')

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

Увидев, что мы запускаем acinc- функции fun1 и fun2, они бы сами обернули их в:

    task1 = asyncio.create_task(fun1(4))
    task2 = asyncio.create_task(fun2(4))

    await task1
    await task2

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

В JavaScript async / await сделаны жадными как Promise. При вызове async функции автоматически создается задача и отправляется в очередь на исполнение в event loop. await, в свою очередь, просто ждёт результат.

В питоне асинхронщину задизайнили иначе - лениво.

Вызов async функции возвращает объект - корутину, - которая ни чего не делает.

asyncio.run() создаёт event loop, запускает (корневую) корутину и блокирует поток до получения результата.

await запускает корутину изнутри другой корутины в текущем event loop и ждёт результат.

Для запуска корутины без ожидания (как это делает Promise) используется asyncio.create_task(coro). Либо asyncio.gather(*aws), если надо запустить сразу несколько. Нужно только следить, чтобы ссылка на возвращаемое значение сохранялась до конца вычисления, иначе его пожрет GC и все оборвется на самом интересном месте (промис бы отработал до конца не смотря ни на что).

В JS только один event loop, поэтому было вполне разумно закопать его внутрь promise / async / await как деталь реализации, упростив работу прикладному программисту. В питоне отзеркалили более ранний вариант корутин на генераторах, дали возможность использовать разные event loop и выставили все кишки наружу.

Feature это адаптер для вычислений, через который с ними взаимодействует event loop. По завершении вычисления в объекте Feature сохраняется его результат (или брошенное на произвол судьбы исключение). В отличие от джаваскриптовых Promise, которые выражают отложенное значение, фичи это процессы вычисления (т.е. их можно принудительно обрывать методом .cancel()).

Task это адаптер для корутин, представляющий их в виде Feature для отправки в event loop. Промежуточные значения, генерируемые корутиной, отправляются в event loop для дальнейшего вычисления. Результат последнего сохраняется в качестве результата всей задачи.

Coroutine - можно рассматривать как функцию с многократными выходами и входами. Или по-простому, генератор последовательности значений, где в точках возврата промежуточных значений используется слово await (раньше было yield, и настоящие генераторы asyncio тоже до сих пор поддерживает). Разница в том, что через yield из генератора можно вернуть любое значение, а через await из async функции только awaitable (т.е feature, coro или task) - защита event loop от дурака.

Столько коментариев, а про twisted даже никто и не вспомнил. Хотя он с в сравнении с asyncio как мерседес с запорожцем. По поводу введения в асинхронное программирование есть отличная статья https://glyph.twistedmatrix.com/2014/02/unyielding.html . Async/await + asyncio хорош тем, что человек ранее писавший только синхронный код легко его может начать писать асинхронный. Но вот дальше будут проблемы с тем, что большинство не понимает в каком контексте этот асинхронный код запускается и что сломается если такой обработчик заблокируется и/или в какой момент передается/возвращается управление в event_loop.

Ну и в отличии от twisted asyncio это голый event-loop без асинхронной реализации библиотек, протоколов и тд.

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

@vlakir читаю ваши слова:

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

Возможно я что-то неверно понимаю, но офиц. документация в разделе awaitables говорит что

  • coroutine function: an async def function;

А вот возвращаемый результат именуют:

  • coroutine object: an object returned by calling a coroutine function.

Да, они помечают это словами "In this documentation " , но мне кажется, что назвать корутиной саму функцию не такая уж и неправильная затея.

Ну так автор и написал что с пациентом нечего не случится если обозвать и функцию и ответ корутиной.

Итак, Пример 2.2.
Можете объяснить, зачем тогда придумали функцию asyncio gather?
Типа когда через обычный вызов,
то сначала выполняется одна строка кода,
пока она не завершится, вторая не выполнится,
а начнёт только после завершения первой?
А при gather эти две строки выполнятся сразу за вдвое меньший промежуток времени (если и у одной, и у второй есть sleep(3) и перейдёт дальше)?

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

Попробую объяснить это так. Если вы делаете вызов

await fun1(42)
await fun2(42)

То получается такая последовательность действий:

  • Создать объект корутины fun1()

  • Запланировать исполнение объекта корутины fun1() в рабочем цикле

  • Вернуть управление в рабочий цикл до завершения объекта корутины fun1()

  • Создать объект корутины fun2()

  • Запланировать исполнение объекта корутины fun2() в рабочем цикле

  • Вернуть управление в рабочий цикл до завершения объекта корутины fun2()

Т.е. пока не закончится выполнение fun1(), выполнение fun2() даже не начнётся - они выполняются строго последовательно. Если же использовать gather(), последовательность действий будет иная:

  • Создать объект корутины fun1()

  • Создать объект корутины fun2()

  • Запланировать исполнение объекта корутины fun1() в рабочем цикле

  • Запланировать исполнение объекта корутины fun2() в рабочем цикле

  • Вернуть управление в рабочий цикл до завершения обоих объектов

Т.е. выполнение fun2() начинается, как только fun1() вернёт управление в цикл, в нашем случае с помощью sleep(). После этого fun1() и fun2() исполняются конкуррентно.

А пример с create_task() по сути делает то же самое, что и gather() - т.е. я бы сказал что gather() это просто удобная обёртка.

Статья просто супер. С нетерпением жду продолжения.

async with aiohttp.ClientSession() as session:
    async with session.get(url) as response:  
       weather_json = await response.json()

Я пробовал писать небольшое асинхронное приложение. Был неприятно удивлен количеством уровней вложенности. Чтобы просто HTTP запрос сделать уже 2 уровня вложенности получается, запрос к базе тоже оборачивается в "async with async_session()".

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

  const response = await fetch('/movies');
  const movies = await response.json();

Вам ничто не мешает написать нужные обёртки. А Python просто даёт больше гибкости.

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

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

aiofiles - построен полностью на тредпуле, это не мешает ему быть конкурентным.

aiofile - использует caio под капотом, который для linux использует системные вызовы ядра для асинхронной работы с файлами, или тредпул на си, для мака тредпул на си или реализацию на python тредах для windows.

По поводу баз данных, тоже не правда, sqlalchemy>1.4 тоже не использует никаких тредов если только драйвер не использует их (aiosqlite построен на потоках, иначе никак)

На самом деле никакого параллельного выполнения чего бы то ни было в питоне нет и быть не может. Кто не верит — погулите аббревиатуру GIL.

Это тоже довольно популярное заблуждение, тот-же упомянутый выше caio отпускает GIL когда читает из фалов, ему его держать не нужно, просто отдал в ядро системный вызов и потом как на eventfd тригернется epoll приходи за результатом.

GIL не является проблемой в python, то что вы не можете его выключить для python кода, это правда, если вам нужна, например, математика многотопочная, берите numpy/numba/cython, там или GIL уже отключен при cpu-bound вызовах, или можно его отключить ( "with nogil:" в Cython).

Множество стандартных функций интерпретатора отпускает GIL, та-же работа с файлами, os.pread например, да и вообще поищите в исходниках интерпретатора Py_BEGIN_ALLOW_THREADS все что в теле под этим макросом, выполняется конкурентно безо всякого GIL, и да, sqlite в их числе.

После 20 лет в JavaScript так приятно видеть что наконец-то Питон его догнал (шутка). Микротаски и Promise.race, Promise.allSettled на очереди.
А на деле, большое спасибо за статью, с футурами чуть яснее стало после вашей аналогии, ну и event_loop без внимания таки оставить не удастся.
Вчера только бодался в pytest с ошибками.
got Future <Future pending cb=[Protocol._on_waiter_completed()]> attached to a different loop

Пытаюсь переписать со "старомодного" способа на `asyncio.run()`. Но не нравится что получается даже более громоздко.
Вместо:

loop = get_or_create_eventloop()
validation_status = loop.run_until_complete(asyncio.gather(
    is_access_token_valid(access_token),
    is_id_token_valid(id_token),
))

Получается:

async def main():
    return await asyncio.gather(
        asyncio.create_task(is_access_token_valid(access_token)),
        asyncio.create_task(is_id_token_valid(id_token)),
    )

validation_status = asyncio.run(main())

Хотелось бы написать так, но это не работает:

validation_status = asyncio.run(
    asyncio.gather(
        asyncio.create_task(is_access_token_valid(access_token)),
        asyncio.create_task(is_id_token_valid(id_token)),
    )
)

Благодоря этой статье, получил главный ответ на свой вопрос. От того что я написал асинхронную функцию, она все равно будет выполняться синхронно. Асинхронность проявляется лишь тогда, когда таких функции несколько, когда создается конкуренция нескольких функции. Раньше мне казалось, что после await должно начаться какая то магия, она есть, но для не для самой функции, а для другой

А разве при использовании gather нужно создание задач? Вот такой код работает примерно за 3 секунды и без create_task:

import asyncio
import datetime


async def fun1():
    print(1)
    await asyncio.sleep(3)


async def fun2():
    print(2)
    await asyncio.sleep(3)


async def main():
    await asyncio.gather(fun1(), fun2())


begin = datetime.datetime.now()
asyncio.run(main())
end = datetime.datetime.now()
print(end - begin)

Спасибо, все просто и хорошо разложили.

У меня вопрос по правильному подходу к обработке ошибок, например если в последнем примере с получением погоды для списка городов. Как обработать ошибку 5xx и продолжить цикл?

Хороший подход, понравилась Парадигма избегать раньше времени погружения в поиск Цикла. Отвлекает от Сути. Браво!

Sign up to leave a comment.

Articles