Когда выбирать 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 ускоряет вычисления: это конкурентность, а не параллелизм