Какой механизм в asyncio сообщает циклу событий о завершении корутины?

Ответ

Цикл событий в asyncio узнает о завершении корутины через объекты Task и Future.

Ключевые компоненты механизма:

  1. Future: Это объект-заместитель, который представляет конечный результат асинхронной операции. Он может находиться в состоянии ожидания (pending) или завершения (finished) и хранить либо результат, либо исключение.

  2. Task: Это подкласс Future, специально предназначенный для выполнения корутин в цикле событий. Когда вы создаете задачу с помощью asyncio.create_task(my_coro()), вы, по сути, говорите циклу событий: "Вот корутина, начни ее выполнять и дай мне Task, чтобы я мог отслеживать ее состояние".

Как это работает:

  • Когда корутина завершается (через return или raise Exception), обернувший ее Task перехватывает результат или исключение.
  • Task устанавливает этот результат в свой внутренний Future и меняет свое состояние на "завершено".
  • Task уведомляет цикл событий о своем завершении, вызывая специальные коллбэки. Если какая-то другая корутина ожидала эту задачу через await, ее выполнение возобновляется.

Пример:

import asyncio

async def fetch_data():
    print("Начинаю загрузку данных...")
    await asyncio.sleep(2)  # Имитация долгой операции
    print("Данные загружены")
    return {"data": 123}

async def main():
    print("Создаю задачу для fetch_data")
    # 1. Корутина оборачивается в Task и планируется к выполнению
    task = asyncio.create_task(fetch_data())

    # 2. main() приостанавливается и ждет, пока task не завершится
    #    К task привязывается коллбэк, который возобновит main()
    result = await task

    # 3. После завершения task, main() возобновляется, и результат доступен
    print(f"Задача завершена: {task.done()}")
    print(f"Полученный результат: {result}")

asyncio.run(main())

Таким образом, цикл событий не "опрашивает" задачи, а реагирует на события их завершения через систему коллбэков, привязанных к объектам Future/Task.

Ответ 18+ 🔞

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

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

Так вот, этот Future — это, блядь, накладная. Просто бумажка, на которой написано: «Результат работы грузчика Иванова». Пока грузчик Иванов таскает коробки, накладная пустая, состояние — «в процессе». Как только он закончил — он пишет на ней «всё, сделал, вот 10 коробок» или «ёбта, ногу сломал» и кладёт её на стол заведующему.

А Task — это сам грузчик Иванов, но с приколоченной к спине этой самой накладной! То есть, когда ты пишешь asyncio.create_task(my_coro()), ты не просто говоришь «Иванов, иди работать». Ты говоришь: «Иванов, иди работать, и вот тебе накладная, которую ты сам и заполнишь, когда закончишь». И заведующий (цикл событий) сразу видит: ага, вот идёт Иванов, и у него на спине бумажка торчит. За ним удобно следить.

Как вся эта ёперная катавасия работает:

  • Грузчик (корутина) закончил таскать коробки (или накосячил и упал в люк). Что он делает? Он берёт эту накладную (Future) со своей спины и пишет на ней результат: «10 коробок» или «Исключение: сломал ногу».
  • Потом он орет: «Эй, начальник, готово!» — и вешает эту заполненную накладную на специальную гвоздику у стола заведующего (вызывает коллбэк).
  • Заведующий (цикл событий) видит: о, на гвоздике накладная от Иванова висит. Он снимает её, смотрит. Если там другой грузчик Пётр ждал, пока Иванов закончит, чтобы начать свою работу (await task), то заведующий тычет Петра в бок: «Проснись, мудила! Иванов закончил, вот его накладная, иди работай дальше».

Пример, чтобы вообще всё встало на свои места:

import asyncio

async def fetch_data():
    print("Начинаю загрузку данных...")
    await asyncio.sleep(2)  # Представь, что он пошёл на перекур на 2 часа
    print("Данные загружены")
    return {"data": 123}  # Вернулся с перекура и написал на накладной: "Данные"

async def main():
    print("Создаю задачу для fetch_data")
    # 1. Берем корутину fetch_data, наряжаем её в жилетку с накладной (Task) и говорим: "Вали на работу!"
    task = asyncio.create_task(fetch_data())

    # 2. main() говорит: "Я подожду тут, пока этот чувак с накладной не закончит".
    #    По сути, он вешает на ту же гвоздику записку "Разбуди main, когда task готов".
    result = await task

    # 3. Цикл событий будит main, суёт ему в руки заполненную накладную от task.
    print(f"Задача завершена: {task.done()}")  # Проверяем, а не обманули ли нас
    print(f"Полученный результат: {result}")   # Читаем, что там грузчик написал

asyncio.run(main())

Короче, цикл событий не бегает за каждым грузчиком с вопросом «ну чё, как дела?». Он сидит, пьёт чай, и ждёт, когда они сами принесут ему заполненные бумажки. Вся хитрая жопа системы — в этих бумажках-накладных (Future), которые приколочены к работникам (Task).