Как устроен event loop в 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()) # запланирована, пойдёт конкурентно
Цикл событий держит очередь готовых задач и регистрирует ожидания в селекторе ОС. Один оборот:
- выполнить по очереди готовые колбэки и шаги задач;
- каждая задача работает до
awaitна чём-то незавершённом — тогда она приостанавливается, а её Future регистрируется на событие; - спросить у ОС, какие сокеты готовы (с таймаутом до ближайшего таймера), и пометить соответствующие задачи готовыми.
Что значит 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 минут бесплатно.