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

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

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

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

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 — это не просто контейнеры, а примитивы синхронизации для безопасного обмена данными между корутинами. В отличие от потоков, где очереди требуют блокировок, asyncio очереди используют 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 и осторожное использование примитивов синхронизации. В продакшне всегда тестируйте производительность, особенно при высоком параллелизме, и не забывайте про обработку ошибок и отмены задач.