Вопрос с собеседования · Python

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

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

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

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

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

Подробно

Корутина — результат вызова 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.
  • Не сохранять ссылку на созданную задачу.

Проверьте себя

Один верный вариант. Подумайте, потом откройте ответ.

Показать варианты и ответ
  • ✗Event loop прерывает задачи по таймеру, как планировщик ОС, поэтому долгий цикл без await не мешает другим задачам
  • ✗Event loop запускает каждую корутину в собственном потоке
  • ✓Один поток выполняет задачи до ближайшего await, ожидание ввода-вывода отдаёт ОС; переключение кооперативное
  • ✗Корутина начинает выполняться сразу при вызове, без await и Task

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

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

Партнёр платформы

Разбор собеседования с sotto

После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.

Разбор собеседования