ProxyForge

Заметки о разработке, инструментах и работе с данными.

Заметки про asyncio: что я перестал путать

За год работы с асинхронным Python накопился список граблей, на которые я больше не хочу наступать. Записываю, чтобы не забыть.

1. await в цикле — это не параллелизм

Самая частая ошибка: ожидание результата по очереди вместо параллельного запуска. Если в цикле for стоит await — все таски выполняются последовательно.

# Это не параллельно. Это просто медленно.
results = []
for url in urls:
    results.append(await fetch(url))

# Параллельно:
results = await asyncio.gather(*(fetch(u) for u in urls))

2. Блокирующие вызовы убивают event loop

Любой requests.get(), time.sleep() или синхронный psycopg2-запрос остановит обработку всех остальных корутин. Если нет асинхронной версии библиотеки — заворачиваем через asyncio.to_thread или executor.

3. asyncio.create_task без сохранения ссылки

Документация прямо предупреждает: если не сохранить ссылку на созданный task, GC может его собрать. Я веду self._tasks: set и складываю туда таски с callback-ом на удаление.

4. Cancellation — это исключение

Когда таску отменяют, в нём поднимается CancelledError. Если есть try/except Exception без except CancelledError отдельно — отмена не сработает. В Python 3.8+ CancelledError наследуется от BaseException, что помогло, но осторожность всё равно нужна.

5. Контекст-менеджеры в async — не такие, как кажется

Если объект поддерживает только синхронный __enter__, внутри корутины он будет работать «как есть», но с риском заблокировать loop. Для I/O-ресурсов нужен async with и __aenter__.

Никаких гениальных откровений — просто пять граблей, которые съели у меня в сумме недели отладки.