← Назад к списку
ТехническаяPythonMiddle

Когда выбирать asyncio, а когда потоки или процессы? Как работает событийный цикл?

Короткий ответ

  • asyncio — кооперативная многозадачность в одном потоке
  • Корутина отдаёт управление циклу на await
  • Подходит для тысяч одновременных I/O-операций
  • Блокирующий вызов в корутине останавливает весь цикл
  • Потоки — для блокирующего I/O, процессы — для CPU
  • gather, TaskGroup и create_task запускают задачи конкурентно

asyncio эффективен для массового сетевого I/O за счёт кооперативного переключения на await, но одна блокирующая операция останавливает весь событийный цикл.

Как сказать вслух

пример ответа

asyncio — это конкурентность в одном потоке: событийный цикл по очереди выполняет корутины, а переключается между ними в точках await, пока одна задача ждёт сеть или таймер. Это дёшево — можно держать тысячи соединений без тысяч потоков. Главное правило: внутри корутин нельзя вызывать блокирующий код, иначе встанет весь цикл — для такого есть run_in_executor или потоки. А для тяжёлых вычислений asyncio не поможет, там нужны процессы.

Подробный ответ

Основной ответ

Событийный цикл (event loop) хранит очередь готовых задач и следит за дескрипторами через системный селектор (epoll/kqueue). Корутина — функция async def; await приостанавливает её и возвращает управление циклу, который запускает следующую готовую задачу. Переключение явное и дешёвое, поэтому asyncio масштабируется до десятков тысяч соединений — идеален для веб-сервисов, парсеров, шлюзов. Ограничения: это один поток, и синхронный вызов (requests, time.sleep, тяжёлый расчёт) блокирует всё; нужны асинхронные библиотеки (httpx, asyncpg) или вынос в executor. Задачи запускают через asyncio.gather, asyncio.TaskGroup (3.11+) или create_task. Правило выбора: asyncio — много сетевого I/O, threading — немного блокирующего I/O, multiprocessing — CPU.

Ключевые моменты

  • Кооперативность. Задачи переключаются только на await — нет гонок на уровне интерпретатора, но «жадная» задача блокирует остальных.
  • Блокирующий код. requests или time.sleep в корутине останавливает цикл; используют await asyncio.sleep, httpx или loop.run_in_executor.
  • Структурированная конкурентность. asyncio.TaskGroup (Python 3.11+) гарантирует завершение и отмену дочерних задач и агрегирует ошибки в ExceptionGroup.
  • Выбор инструмента. asyncio — тысячи соединений; потоки — пара блокирующих вызовов; процессы — параллельные вычисления.

Практический контекст

Это основа FastAPI, aiohttp и современных ботов/парсеров. На собеседовании часто спрашивают, что будет, если в async-эндпоинте вызвать requests или time.sleep, и как ограничить число одновременных запросов (Semaphore). Интервьюер проверяет не синтаксис, а понимание модели: почему await обязателен и чем конкурентность отличается от параллелизма.

Пример кода

import asyncio, httpx

async def fetch(client, url):
    r = await client.get(url)
    return r.status_code

async def main():
    async with httpx.AsyncClient() as client:
        results = await asyncio.gather(
            *(fetch(client, f"https://example.com/{i}") for i in range(10))
        )
    print(results)

asyncio.run(main())

Частые ошибки

  • Вызывают блокирующие библиотеки внутри корутин и удивляются деградации сервиса
  • Забывают await перед корутиной — она создаётся, но не выполняется
  • Считают, что asyncio ускоряет вычисления: это конкурентность, а не параллелизм

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru