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())

Что здесь происходит:

  1. async def создает асинхронную функцию
  2. await asyncio.sleep(1) говорит: можно поставить задачу на паузу и дать event loop заняться чем-то другим
  3. 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-функций нужно использовать осторожно

Подписаться
Уведомить о
guest
0 комментариев
Старые
Новые Популярные

Настройки cookie

0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x