Logo Craft Homelab Docs Контакты Telegram
Asyncio: глубокое погружение — Event loop, tasks Трендовые github проекты в нашем телеграм канале. Подпишись →
6 февраля 2026 г.

Asyncio: глубокое погружение

Асинхронный код в Python — отдельная модель выполнения: один поток, явные точки передачи управления через await, без накладных расходов на переключение контекстов. Многие разработчики используют asyncio как “черный ящик”, не понимая как event loop управляет задачами, почему await не блокирует поток и что происходит под капотом при переключении контекстов. Это приводит к неэффективному коду, трудноотлавливаемым багам и проблемам при масштабировании.

Event Loop: сердце асинхронности

В отличие от Node.js, где event loop — это часть C++, в Python event loop — это полностью Python-объект, что дает гибкость, но и накладывает ограничения.

Когда вы вызываете asyncio.run(main()), происходит следующее:

  1. Создается новый event loop (или берется существующий)
  2. В него добавляется ваша основная корутина
  3. Event loop начинает выполнять код до первой точки await
  4. Встретив await, event loop приостанавливает выполнение текущей корутины и переходит к следующей запланированной задаче
  5. Когда awaited-объект готов (например, данные с сервера пришли), event loop ставит корутину в очередь на выполнение

Ключевое отличие: в asyncio переключение контекста происходит не по таймауту, а в явных точках await. Это позволяет избегать накладных расходов на контекстные переключения как в потоках, но требует от разработчика дисциплины в написании кода.

import asyncio
import time

# Демонстрация event loop
async def slow_operation():
    print("Начало операции")
    await asyncio.sleep(1)  # Явная передача управления event loop
    print("Операция завершена")
    return "Результат"

async def main():
    print("Запуск main")
    start_time = time.time()

    # Event loop автоматически управляет задачами
    result = await slow_operation()

    print(f"Результат: {result}")
    print(f"Время выполнения: {time.time() - start_time:.2f} секунд")

asyncio.run(main())

Подводные камни event loop

  1. Блокирующий код — любой синхронный код внутри корутины блокирует весь event loop. Для таких случаев существуют loop.run_in_executor(), но это усложняет архитектуру

  2. Вложенные циклы — нельзя запускать event loop изнутри другого event loop. Это распространенная ошибка при тестировании или интеграции с другими библиотеками

  3. Ресурсное голодание — если одна корутина не передает управление через await, другие задачи могут “задыхаться”

Tasks: управление конкурентностью

Task — это обертка над корутиной с расширенным функционалом. Когда вы создаете task через asyncio.create_task(), корутина регистрируется в event loop и начнёт выполняться при следующем цикле — независимо от текущей корутины.

import asyncio

async def coro1():
    print("Корутина 1 запущена")
    await asyncio.sleep(1)
    print("Корутина 1 завершена")
    return "Результат 1"

async def coruto2():
    print("Корутина 2 запущена")
    await asyncio.sleep(0.5)
    print("Корутина 2 завершена")
    return "Результат 2"

async def main():
    # Создаем tasks
    task1 = asyncio.create_task(coro1())
    task2 = asyncio.create_task(coruto2())

    # Ждем завершения обеих задач
    results = await asyncio.gather(task1, task2)
    print(f"Получены результаты: {results}")

asyncio.run(main())

Важные аспекты Tasks

  1. Создание и жизненный цикл — task создается в состоянии “pending” и добавляется в event loop. При первом await она начинает выполняться. Task может быть отменен через task.cancel()

  2. Отмена и обработка CancelledError — при отмене task поднимается CancelledError, который можно обработать в корутине через try/except

  3. Асинхронный joinasyncio.gather() — параллельный запуск нескольких tasks с барьером: ждёт все, собирает ошибки

  4. Callback-и — tasks поддерживают callback-и через task.add_done_callback(), что полезно для логирования и очистки ресурсов

import asyncio

async def long_task():
    try:
        print("Долгая задача началась")
        await asyncio.sleep(5)
        print("Долгая задача завершилась")
    except asyncio.CancelledError:
        print("Задача отменена")
        raise

async def main():
    task = asyncio.create_task(long_task())

    # Отменим задачу через 2 секунды
    await asyncio.sleep(2)
    task.cancel()

    # Дадим шанс обработать отмену
    try:
        await task
    except asyncio.CancelledError:
        print("Отмена обработана в main")

    print("Завершение main")

asyncio.run(main())

Queues: безопасная передача данных

Очереди в asyncio — примитивы синхронизации: producer кладёт данные, consumer забирает, event loop управляет уведомлениями без блокировок.

Типы очередей

  1. Queue — FIFO очередь с неограниченным или ограниченным размером
  2. PriorityQueue — очередь с приоритетами на основе ключей
  3. LifoQueue — очередь LIFO (последним пришел - первым ушел)
import asyncio
import random

async def producer(queue):
    for i in range(5):
        item = random.randint(1, 10)
        print(f"Производитель добавил: {item}")
        await queue.put(item)
        await asyncio.sleep(random.uniform(0.1, 0.5))

async def consumer(queue):
    while True:
        item = await queue.get()
        print(f"Потребитель получил: {item}")
        # Обработка элемента
        await asyncio.sleep(random.uniform(0.5, 1.0))
        queue.task_done()

async def main():
    queue = asyncio.Queue(maxsize=3)

    # Создаем producer и consumer
    producer_task = asyncio.create_task(producer(queue))
    consumer_task = asyncio.create_task(consumer(queue))

    # Ждем завершения producer
    await producer_task

    # Ждем обработки всех элементов
    await queue.join()

    # Отменяем consumer
    consumer_task.cancel()
    await consumer_task

    print("Завершение работы")

asyncio.run(main())

Особенности использования очередей

  1. Блокировкиqueue.get() блокирует выполнение, пока элемент не станет доступен. Это ключевое отличие от обычных очередей

  2. Размер очереди — ограниченный размер очереди предотвращает переполнение памяти, но требует аккуратной обработки в producer

  3. Завершение работыqueue.join() полезен для определения, когда все элементы обработаны

Производственные паттерны и антипаттерны

Правильное использование

  1. Ресурсные пулы — для соединений с базой данных или HTTP-клиентов используйте пул соединений, созданный внутри event loop

  2. Семантика fire-and-forget — для задач, не требующих результата, используйте asyncio.create_task() без await

  3. Ограничение параллелизма — используйте asyncio.Semaphore() для контроля количества одновременных запросов

Распространенные ошибки

  1. Блокирующий код — синхронные вызовы внутри корутин. Даже time.sleep() блокирует event loop

  2. Избыточное создание задач — создание тысяч мелких tasks вместо группировки в батчи

  3. Неправильная обработка исключений — необработанное исключение в task “убивает” всю группу в asyncio.gather()

# Антипаттерн: блокирующий код внутри async функции
async def bad_example():
    import time
    time.sleep(1)  # Блокирует event loop!
    # ...

# Правильный подход
async def good_example():
    await asyncio.sleep(1)  # Не блокирует event loop
    # ...

Узкие места и компромиссы

  1. GIL — для CPU-bound операций asyncio не помогает. Рассматривайте multiprocessing или Cython

  2. Отладка — асинхронный код сложнее отлаживать из-за нелинейного выполнения. Используйте asyncio.Task.all_tasks() и логирование

  3. Сложность — асинхронный код требует большей дисциплины и понимания внутренних механизмов

  4. Смешивание моделей — интеграция с callback-based библиотеками через asyncio.to_thread() или loop.run_in_executor()

Заключение

Asyncio выигрывает на IO-bound операциях: сетевые запросы, работа с файлами, БД. Для CPU-bound задач или простых скриптов — избыточен.

Ключ к успешному использованию asyncio — понимание event loop, правильное управление tasks и осторожное использование примитивов синхронизации. В продакшне всегда тестируйте производительность, особенно при высоком параллелизме, и не забывайте про обработку ошибок и отмены задач.