Python
Event loop в Python: что это и как работает
Event loop в Python — это механизм, который управляет асинхронными задачами. Он запускает одну задачу, доходит до места ожидания, переключается на другую задачу, потом возвращается к первой, когда та снова готова продолжить работу. В Python чаще всего с event loop работают через модуль asyncio
Проще всего думать так: event loop не делает медленную операцию быстрее саму по себе, но не дает программе простаивать, пока она ждет сеть, таймер, файл, базу данных или ответ API
По этой теме полезно отдельно посмотреть Составьте еженедельное расписание занятий, чтобы расширить контекст и сравнить подходы
По этой теме полезно отдельно посмотреть Аннотации типов для декораторов в Python, чтобы расширить контекст и сравнить подходы
Где event loop полезен
Асинхронность особенно полезна там, где программа много ждет:
- бот ждет сообщения пользователей
- скрипт отправляет несколько сетевых запросов
- веб-сервис обслуживает много соединений
- приложение ждет ответы от API
- программа работает с очередями и таймерами
Если задача нагружает процессор — например, считает большой отчет, кодирует видео или обрабатывает миллионы чисел — event loop сам по себе не ускорит вычисления. Для CPU-тяжелых задач нужны другие подходы: оптимизация алгоритма, процессы, расширения, очереди или отдельные воркеры
Минимальный пример asyncio
import asyncio
async def say_hello():
await asyncio.sleep(1)
print('Привет')
asyncio.run(say_hello())
Что здесь происходит:
async defсоздает асинхронную функциюawait asyncio.sleep(1)говорит: можно поставить задачу на паузу и дать event loop заняться чем-то другимasyncio.run()создает event loop, запускает асинхронную функцию и закрывает loop после завершения
asyncio.sleep(1) похож на обычное ожидание, но оно не блокирует event loop. В реальном коде вместо него часто будет сетевой запрос, чтение из очереди или ожидание ответа от сервиса
Coroutine, task и await
В асинхронном Python часто встречаются три слова
| Термин | Что означает |
|---|---|
| coroutine | результат вызова асинхронной функции |
| await | ожидание результата coroutine или task |
| task | coroutine, запланированная в event loop |
Если просто вызвать асинхронную функцию, она не выполнится сразу
async def load_data():
return 'данные'
result = load_data()
print(result)
На экране будет объект coroutine, а не строка данные. Чтобы получить результат, нужно использовать await внутри другой асинхронной функции
import asyncio
async def load_data():
return 'данные'
async def main():
result = await load_data()
print(result)
asyncio.run(main())
Последовательное ожидание
Посмотрим на два ожидания подряд
import asyncio
import time
async def load_page(number):
await asyncio.sleep(1)
return f'страница {number}'
async def main():
start = time.perf_counter()
page1 = await load_page(1)
page2 = await load_page(2)
print(page1, page2)
print(round(time.perf_counter() - start, 2))
asyncio.run(main())
Две операции займут примерно две секунды, потому что вторая начинается только после первой
Такой код асинхронный по форме, но не параллельный по ожиданию. await внутри main() останавливает выполнение main(), пока конкретная операция не завершится
Одновременное ожидание через gather
Чтобы запустить несколько ожиданий вместе, используйте asyncio.gather()
import asyncio
import time
async def load_page(number):
await asyncio.sleep(1)
return f'страница {number}'
async def main():
start = time.perf_counter()
pages = await asyncio.gather(
load_page(1),
load_page(2),
load_page(3),
)
print(pages)
print(round(time.perf_counter() - start, 2))
asyncio.run(main())
Результат будет примерно таким:
['страница 1', 'страница 2', 'страница 3']
1.0
Три задачи ждут одновременно, поэтому общее время близко к одной секунде, а не к трем. Это и есть практический смысл event loop для задач ввода-вывода
create_task: запланировать задачу
asyncio.create_task() создает task и отправляет ее в event loop
import asyncio
async def notify():
await asyncio.sleep(1)
print('уведомление отправлено')
async def main():
task = asyncio.create_task(notify())
print('основная работа')
await task
asyncio.run(main())
Сначала задача планируется, потом main() продолжает работу. Когда нужен результат или нужно дождаться завершения, используется await task
Если создать task и никогда ее не ждать, легко получить странное поведение: программа может завершиться раньше, чем задача выполнится
Что блокирует event loop
Самая частая проблема — вызвать обычную блокирующую функцию внутри асинхронного кода
Плохой пример:
import asyncio
import time
async def bad_wait():
time.sleep(2)
print('готово')
asyncio.run(bad_wait())
time.sleep(2) блокирует весь поток. Event loop не может переключиться на другие задачи
Лучше:
import asyncio
async def good_wait():
await asyncio.sleep(2)
print('готово')
asyncio.run(good_wait())
То же правило касается библиотек. Если вы пишете асинхронный код, используйте асинхронные клиенты для HTTP, баз данных и очередей. Обычный блокирующий клиент может испортить пользу от event loop
Частые ошибки
Вызвать await вне async def
await asyncio.sleep(1)
Так нельзя. await работает только внутри асинхронной функции
Вызвать asyncio.run() внутри уже работающего event loop. Такое часто встречается в Jupyter Notebook, некоторых веб-фреймворках и ботах. Там event loop уже запущен, поэтому нужно использовать правила конкретной среды
Думать, что async ускорит CPU-тяжелый код. Если функция долго считает и нигде не делает await, event loop не сможет переключаться
Забыть await перед coroutine и получить предупреждение, что coroutine never awaited
Асинхронность, потоки и процессы
Event loop обычно работает в одном потоке и переключает задачи в точках await. Потоки позволяют выполнять несколько операций в разных потоках, а процессы — использовать несколько процессов и обходить ограничения одного интерпретатора для CPU-задач
Для сетевых ожиданий часто хватает asyncio. Для блокирующих библиотек иногда удобнее потоки. Для тяжелых вычислений чаще нужны процессы или отдельные воркеры
Выбор зависит не от модности подхода, а от узкого места. Если программа ждет сеть, event loop хорош. Если программа считает, event loop не решит проблему сам
Практические задачи
Напишите две асинхронные функции с разной задержкой и запустите их через gather()
Сравните последовательный await и asyncio.gather() по времени
Создайте task через asyncio.create_task() и дождитесь ее через await
Замените time.sleep() на asyncio.sleep() в асинхронной функции
Напишите мини-скрипт, который «загружает» пять страниц через asyncio.sleep() и возвращает список результатов
Event loop в Python управляет асинхронными задачами и помогает не простаивать во время ожидания. Он особенно полезен для сетевых запросов, ботов, веб-сервисов, очередей и других задач ввода-вывода
Запомните связку: async def создает асинхронную функцию, await ждет результат без блокировки event loop, asyncio.run() запускает главный сценарий, gather() помогает ждать несколько задач вместе, а блокирующий код внутри async-функций нужно использовать осторожно