Это не обзор asyncio, а три ситуации, в которых я потерял время. Все три выглядят как «код работает», пока не посмотришь внимательно.
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).
Ошибка, из-за которой задача молча не работает:
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.
Самый коварный случай: await стоит, но сайт всё равно тормозит.
async def handle(request):
data = requests.get("https://api.example.com") # синхронно!
return web.json_response(data.json())
requests.get блокирует весь event loop — на время запроса не выполняется ничего: ни другие обработчики, ни таймауты. Асинхронным код делает не ключевое слово async, а то, что внутри не блокирует поток.
Что обычно стоит заменить:
| Блокирует | Чем заменить |
|---|---|
requests | httpx.AsyncClient или aiohttp |
time.sleep | asyncio.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
gather без return_exceptions=True — не «выполнить всё», а «выполнить всё или отменить».create_task и ссылка на задачу.Хорошая привычка: при подозрительной «медленности» асинхронного сервиса снять профилировщик py-spy dump --pid — он честно покажет, где поток стоит в синхронном вызове.