Заметки про 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__.
Никаких гениальных откровений — просто пять граблей, которые съели у меня в сумме недели отладки.