В чем выполняются корутины?

Пусть есть код:

import asyncio


async def fetch():
    await asyncio.sleep(1)


async def main():
    await asyncio.gather(*[fetch() for _ in range(5)])


asyncio.run(main())

Он выполнится за ~1 секунду.

С одной стороны код запускается в одном процессе и одном потоке. С другой стороны функции fetch "спят" параллельно. Здесь у меня возникает непонимание. Знаю, что asyncio.sleep неблокирующая функция. Но как реализуется способность не блокировать поток? В чем "спит" функция asyncio.sleep?


Ответы (1 шт):

Автор решения: eri

В 2х словах слово await выполняет 2 действия: входит в цикл ( который выполняет другие асинхронные функции) и возвращается в функцию которая его вызвала.

Если говорить упрощенно, то выполняет всё loop, который является списком задач.

При каждом слове await программа проваливается к этому циклу.

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

Если готовых нет, то исполняется вход в асинхронные функции и выполнение в них обычного кода до слова await.

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

Почему asyncio.sleep не блокирует?

На каждом входе в loop исполняется встроенный таймер и проверка дескрипторов на готовность к чтению или записи (select и тп).

Таймер проверяет когда должен закончится ближайший sleep и если время пришло, то возвращается в функцию где ждут его await-ом.

Уточнения

дескрипторы - это файлы, сокеты, пайпы, ком порты и вроде именованные каналы.

await - крутит луп пока не исполнится таск, а потом возвращает результат.

Каждая асинхронная функция где-то в глубине упирается в неблокирующий сокет, другой дескриптор или в таймер. На этом месте она прерывается и не выполняется далее.

Луп с помощью select (poll или подобного, зависит от ОС) проверяет записала ли ОС что-то в сокеты, если что-то есть - то уже читает его. select - функция которая выбирает готовый

Питон возвращает на await в функцию где-то в глубине, которая возвращает на другой await и так далее до вашего кода.

Ваш код исполняется до конца функции или до следующего await.

В этом месте если вызвать что-то блокирующее (time.sleep(10)), то loop не провернется. Также если будете выполнять тяжелую математику другие прерванные таски не очнутся. Для циклов с математикой кстати делай await asyncio.sleep(0) для "паралельной" (именно в кавычках) работы

→ Ссылка