asyncio: три грабля, на которые наступил

Python · 16 сентября 2026 · ~6 минут

Это не обзор asyncio, а три ситуации, в которых я потерял время. Все три выглядят как «код работает», пока не посмотришь внимательно.

1. gather отменяет остальное при первой ошибке

Классический случай: отправляем десять писем параллельно, одно падает — и остальные девять не отправляются.

import asyncio

async def send(i):
    if i == 3:
        raise RuntimeError("SMTP down")
    await asyncio.sleep(0.1)
    return i

async def main():
    results = await asyncio.gather(*[send(i) for i in range(10)])
    print(results)

asyncio.run(main())

Поведение по умолчанию — при исключении остальные задачи отменяются. На практике почти всегда нужно иное: выполнить всё, а потом разобраться с ошибками.

results = await asyncio.gather(*coros, return_exceptions=True)
ok = [r for r in results if not isinstance(r, Exception)]
errors = [r for r in results if isinstance(r, Exception)]
Если важна «первое исключение» asyncio.gather(..., return_exceptions=True) гасит ошибки — их надо поднимать руками. Для сценария «стоп при первой» есть asyncio.wait(..., return_when=FIRST_EXCEPTION).

2. Корутина, которую «запустили», но не запустили

Ошибка, из-за которой задача молча не работает:

async def worker():
    print("started")

async def main():
    worker()          # ← просто создали корутину, ничего не запустили
    await asyncio.sleep(0.5)
    print("done")

Вывод будет только done — и никакого предупреждения в большинстве случаев. Нужно явно:

    task = asyncio.create_task(worker())   # или asyncio.ensure_future()
    ...
    await task

Отдельная тонкость: если сохранить задачу в локальную переменную и не дождаться её, сборщик мусора может отменить её в произвольный момент. Практика — держать ссылку в списке или контейнере и дожидаться через asyncio.gather.

3. Блокирующий вызов внутри обработчика

Самый коварный случай: await стоит, но сайт всё равно тормозит.

async def handle(request):
    data = requests.get("https://api.example.com")   # синхронно!
    return web.json_response(data.json())

requests.get блокирует весь event loop — на время запроса не выполняется ничего: ни другие обработчики, ни таймауты. Асинхронным код делает не ключевое слово async, а то, что внутри не блокирует поток.

Что обычно стоит заменить:

БлокируетЧем заменить
requestshttpx.AsyncClient или aiohttp
time.sleepasyncio.sleep
Синхронные драйверы БДАсинхронные (asyncpg, asyncmy) или run_in_executor
open() / чтение файловaiofiles или executor для крупных файлов
hashlib, тяжёлые вычисленияloop.run_in_executor
import concurrent.futures

executor = concurrent.futures.ThreadPoolExecutor(max_workers=8)

async def handle(request):
    loop = asyncio.get_running_loop()
    result = await loop.run_in_executor(executor, blocking_call, arg)
    return result

Короткий итог

Хорошая привычка: при подозрительной «медленности» асинхронного сервиса снять профилировщик py-spy dump --pid — он честно покажет, где поток стоит в синхронном вызове.