Прямой ответ
asyncio даёт выигрыш в одном сценарии: много одновременных операций ожидания ввода-вывода (HTTP-запросы, БД, сеть) в одном потоке — тысячи соединений без тысяч потоков. Для CPU-задач он бесполезен (GIL никто не отменял — см. статью про многопроцессность), а для «пары запросов к API» обычный синхронный код проще и быстрее пишется. Три правила, которые покрывают 90% практики: не блокировать event loop (никаких time.sleep/requests внутри async), использовать современные примитивы (TaskGroup и timeout вместо ручных gather/loop), и проектировать отмену (любой await может быть прерван — ресурсы должны закрываться).
Модель: event loop, корутины, таски
Корутина (async def) — не выполняется, пока не await или не обёрнута в Task; await — точка, где управление может вернуться в loop и заняться другими задачами. Отсюда главный инсайт: конкурентность появляется только там, где есть несколько независимых задач, запущенных одновременно (asyncio.gather(coro1, coro2) или async with asyncio.TaskGroup() as tg: tg.create_task(...)), а последовательные await в одном теле — это просто поочерёдное ожидание. TaskGroup (3.11+) предпочтительнее gather в новом коде: структурная конкурентность — при падении одной задачи отменяются все, и исключения не теряются. Одиночный asyncio.run(main()) — входная точка; крутить свой loop руками почти никогда не нужно.
Главный грабель: блокирующие вызовы
Event loop однопоточен, и любой синхронный вызов внутри корутины останавливает все задачи: time.sleep(1) вместо await asyncio.sleep(1), синхронный HTTP-клиент вместо асинхронного (выбор клиентов — в статье про requests и httpx), тяжёлый парсинг/хэширование, и особенно — синхронные драйверы БД в async-фреймворке. Лечение трёх видов: асинхронная версия библиотеки (httpx, asyncpg, aiofiles); для CPU-тяжёлого и legacy-кода — await asyncio.to_thread(blocking_fn, ...) (вызов в отдельном потоке, 3.9+); для CPU-тяжёлого — ProcessPoolExecutor. Детектор — режим отладки PYTHONASYNCIODEBUG=1 (warnит о медленных коллбэках) и «тормозящий» сервис под нагрузкой при формально низкой загрузке CPU.
Отмена, таймауты и корректность ресурсов
Отмена — часть дизайна asyncio, а не исключительная ситуация: CancelledError прилетает в любую точку await при таймауте/остановке, и код обязан отпускать ресурсы (отсюда async with для всего, что открывается, и осторожность с except Exception без пере-raise отмены — он её проглатывает). Таймауты — обязательны на каждый внешний вызов: async with asyncio.timeout(5): (3.11+) или asyncio.wait_for(coro, 5); «вечное ожидание» одного зависшего соединения — классика продакшн-инцидентов. Отдельная дисциплина — graceful shutdown: сбор незавершённых тасков, отмена, ожидание с ограничением; фреймворки (FastAPI/uvicorn, aiohttp) делают это за вас — самописные loop'ы требуют ручной работы.
Конкурентность с ограничением и фоновые задачи
«Выстрелить 10 000 задач сразу» — путь к исчерпанию файловых дескрипторов и бану по rate limit: ограничивайте конкурентность asyncio.Semaphore(N) или батчами. Фоновые задачи («запусти и забудь») через create_task без ссылки — задача может быть собрана GC, а исключение молча потеряно: держите ссылку и добавляйте done-callback с логированием, либо используйте TaskGroup/очереди. Очереди (asyncio.Queue) — основной паттерн producer/consumer внутри процесса; между процессами — внешние брокеры (Celery/Redis — см. статью про фоновые задачи).
Асинхронные тесты и отладка
Тесты — через pytest-asyncio (@pytest.mark.asyncio и асинхронные фикстуры; настройка — в статье про pytest-фикстуры), мокирование async-кода — AsyncMock. Отладка: asyncio.run(main(), debug=True) ловит незаawait-анные корутины («coroutine was never awaited» — частая ошибка: корутина создана, но не запущена, результат — тихий no-op) и медленные коллбэки; трассировки тасков — через task.get_coro() и логирование имён (asyncio.current_task().get_name()). Правило гигиены: предупреждение «never awaited» в логах — всегда баг, а не шум.
Практический сценарий: конкурентные вызовы внешнего API
Задача: опросить триста карточек во внешнем API, лимит — десять одновременных соединений, у каждого вызова таймаут пять секунд, при сетевой ошибке — две повторные попытки. Сборка на asyncio: семафор на десять внутри функции-обёртки (каждая задача приобретает его перед запросом — лимит соблюдается независимо от числа задач), httpx.AsyncClient создаётся один на весь прогон (пул соединений переиспользуется, а не собирается на каждый запрос), внутри — asyncio.timeout(5) вокруг вызова и ручной цикл ретраев с возрастающей паузой только на сетевые исключения и ответы 5xx. Триста задач заводятся через TaskGroup — при исключении в одной (например, 404 после всех ретраев) группа отменяет остальные, и прогон падает быстро и с понятной ошибкой, а не продолжает молотить впустую. Результаты собираются списком по мере готовности. Такой прогон занимает примерно время самого медленного вызова, умноженное на число «волн» (300/10 × ~1 с ≈ 30 с), тогда как последовательная версия — те же триста вызовов по очереди — шла бы пять минут. И главное отличие от «просто gather на триста задач» — семафор не даёт упереться в лимит файлового дескриптора и не провоцирует бан со стороны API.
FAQ
Стоит ли переводить синхронный сервис на async? Только если профиль нагрузки — много медленного I/O (внешние API, долгие запросы) и метрики это подтверждают. Для CPU-нагрузки, очередей и внутренних сервисов выигрыша нет, а сложность (драйверы, отладка, библиотеки) вырастет.
asyncio или потоки?
Для тысяч соединений — asyncio (поток на соединение не масштабируется). Для десятков параллельных блокирующих операций потоки (ThreadPoolExecutor) проще и не требуют переписывания стека под async.
Почему мой async-код не быстрее синхронного? Почти всегда: задачи выполняются последовательно (нет gather/TaskGroup) или внутри блокирующие вызовы. Проверка — режим debug и честный подсчёт одновременных задач.
Нужен ли async в CLI-скриптах и batch-джобах?
Обычно нет: скрипту достаточно to_thread/пула потоков или просто последовательности с ретраями. async оправдан, когда скрипт держит сотни соединений (краулер, мониторинг).
Источники: документация asyncio (обзор, tasks, debug mode) — ссылки в шапке.