Чтобы понять, как рабоатет asyncio, давайте рассмотрим, как в Python работают функции.
Мы можем вызвать функцию, она выполнится. Просто интерпретатор берет её код и выполняет целиком. Если же мы берём функцию-генератор и просто вызываем, ничего не будет происходить. Тебе надо по ней пробежаться циклом или самому вызывая
next. Когда мы вызываем next, код функции доходит до следующего yield и останавливается.Представим, что у нас есть основной части программы цикл, который крутит этот генератор.
def foo():
yield 1
yield 2
for _ in foo():
pass
Если между
yield вызывается обычная функция, то она выполняется как обычно, и не важно, что это происходит внутри генератора. А что, если наш генератор внутри себя вызывает другой генератор через yield from?def foo():
yield 1
yield 2
def bar():
yield from foo()
for _ in bar():
pass
В этом случае, наш основной цикл просто крутится дальше как раньше, но уже выполняется не код первой функции, а переходит к той, что она вызвала и останавливается уже на её
yield. То есть мы как бы крутим уже вложенный генератор, а не оригинальный. Нашему циклу не важно, откуда берутся шаги внутри функции: то ли она сама их выдумала, то ли получила из вложенных вызовов.Проводя аналогию с asyncio наш главный цикл - это event loop, а генераторы - корутины. Корутины могут друг друга вызвать и циклу норм, он продолжает их крутить. Разница чисто синтаксическая: в asyncio используется
await вместо yield.Для полнцоенного использования тут не хватает одной важной детали.
На текущий момент у нас есть цикл, который постоянно дёргает генератор и при завершении шага переходит сразу к следующему. В реальности же мы хотим приостанавливать выполнение до получения данных из сети, наступления определенного времени и т.п. В норме мы хотим отдать управление циклу событий, чтобы он там что-то ещё мог поделать своё пока мы ждем. Но, как только слип завершится или сокет будет готов, надо вернуться назад и продолжить выполнение.
Итого, мы крутим цикл. Там генераторы, вызывающие генераторы. Мы это все крутим, крутим. Получаем оттуда объект, который наш loop умеет ожидать (Future). И каждый раз, прежде чем крутить дальше, ждём что он будет готов.
А раз нас есть цикл, который умеет ждать между вызовами
next, он может в это время дёрнуть другой генератор, если у него есть их список. То есть, такие независимые генераторы - это таски asyncio.Кратко:
1. Когда ты эвейтишь специальный объект, ты делаешь
yield и отдаешь управление циклу событий2. Когда ты эвейтишь другую корутину, ты просто проваливаешся внутрь
3. Когда цикл событий получает управление, он может заняться другими делами
4. У цикла событий дел два: дергать разные таски и ждать если все просили подождать внешнего мира
Дополнительные материалы:
• https://peps.python.org/pep-3156
• https://peps.python.org/pep-0380
• https://t.me/opensource_findings/869
• https://www.gevent.org/intro.html