Получить доступ

Как устроен event loop в asyncio?

MiddleКонкурентностьasyncio
Ещё спрашивают так:
  • Что происходит при await внутри корутины?
  • Чем корутина отличается от задачи (Task) в asyncio?
Коротко

Event loop — цикл в одном потоке: берёт готовые к выполнению задачи, выполняет каждую до ближайшего await, а ожидающие ввода-вывода передаёт ОС (epoll, kqueue) и возвращается к ним, когда данные пришли. Переключение кооперативное — только в точках await.

  • Корутина — объект, который ничего не делает, пока его не ждут или не обернули в Task
  • Task — корутина, поставленная в расписание цикла: выполняется конкурентно с остальными
  • Пока корутина не дошла до await, все остальные стоят

А ещё asyncio.run создаёт цикл, выполняет главную корутину и закрывает цикл

Так подсказку видит пользователь sotto на живом собеседовании — за пару секунд, пока вы думаете вслух.

Подробно

Корутина — результат вызова async def-функции. Сам вызов ничего не выполняет:

async def fetch(): ...
coro = fetch()        # корутина создана, код не выполнялся
await coro            # выполнится сейчас, последовательно
task = asyncio.create_task(fetch())   # запланирована, пойдёт конкурентно

Цикл событий держит очередь готовых задач и регистрирует ожидания в селекторе ОС. Один оборот:

  1. выполнить по очереди готовые колбэки и шаги задач;
  2. каждая задача работает до await на чём-то незавершённом — тогда она приостанавливается, а её Future регистрируется на событие;
  3. спросить у ОС, какие сокеты готовы (с таймаутом до ближайшего таймера), и пометить соответствующие задачи готовыми.

Что значит await. Если ожидаемое уже готово — выполнение продолжается сразу, без переключения. Если нет — корутина отдаёт управление циклу. Поэтому await на функции без настоящего ожидания внутри не даёт конкурентности.

Следствия однопоточности:

  • между двумя await код выполняется атомарно относительно других задач — гонок за данные меньше, чем в потоках;
  • любой долгий синхронный участок (вычисление, time.sleep, блокирующий драйвер БД) останавливает все задачи;
  • масштабирование по ядрам — несколько процессов, каждый со своим циклом.

Ссылку на Task нужно хранить: цикл держит задачи слабыми ссылками, и «выстрелил и забыл» create_task(...) без сохранения может быть собран сборщиком мусора посреди работы.

Пример

import asyncio

async def worker(name, delay):
    await asyncio.sleep(delay)      # отдаёт управление циклу
    return name

async def main():
    t1 = asyncio.create_task(worker("a", 1))
    t2 = asyncio.create_task(worker("b", 1))
    print(await t1, await t2)        # около 1 секунды, а не 2

asyncio.run(main())

Что хотят услышать

  • Однопоточный цикл и кооперативная многозадачность.
  • Различие корутины и Task.
  • Что происходит на await.
  • Почему блокирующий код останавливает всё.

Частые ошибки

  • Ожидать параллельного выполнения от последовательных await без create_task или gather.
  • Не сохранять ссылку на созданную задачу.

Что спросят дальше

Связанные вопросы

Закрытая бета

Подсказки на собеседовании — в реальном времени

sotto слышит вопрос интервьюера и показывает короткий ответ и план, как в блоке «Коротко». Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем