← Назад к списку
ПрограммированиеPythonSenior

Скачайте 100 URL конкурентно, но не более 10 одновременно, с обработкой ошибок отдельных запросов.

Короткий ответ

  • asyncio.Semaphore(10) ограничивает одновременные запросы
  • Каждый URL — корутина, захватывающая семафор через async with
  • gather с return_exceptions=True не роняет всё из-за одного URL
  • Один AsyncClient на все запросы — переиспользование соединений
  • Таймауты обязательны, иначе задача может висеть вечно
  • Альтернатива: TaskGroup или очередь с пулом воркеров

Семафор внутри корутины ограничивает параллелизм, gather с return_exceptions собирает результаты и ошибки, а общий клиент и таймауты делают решение производственным.

Как сказать вслух

пример ответа

Я создаю семафор на десять разрешений и одну корутину на URL: она захватывает семафор через async with, делает запрос и возвращает результат. Все сто задач запускаю разом через gather — реально одновременно будут работать не больше десяти. Флаг return_exceptions нужен, чтобы один упавший URL не отменил остальные: ошибки вернутся в общем списке, и я их разберу. Обязательно один общий HTTP-клиент и таймауты на каждый запрос.

Подробный ответ

Основной ответ

Структура решения: asyncio.Semaphore(10) как разделяемый лимитер; корутина fetch(sem, client, url) оборачивает запрос в async with sem — лишние задачи ждут свободного слота без занятых потоков. Все корутины передаются в asyncio.gather(..., return_exceptions=True): порядок результатов соответствует порядку URL, а исключения отдельных запросов возвращаются объектами вместо отмены остальных задач. Один httpx.AsyncClient на всю пачку переиспользует соединения (keep-alive, пулы) — создавать клиент на каждый запрос дорого и неправильно. Таймаут задаётся на клиенте или запросе. Продакшен-усиления: ретраи с backoff на сетевые ошибки, разбор результатов на успехи/ошибки, в 3.11+ — asyncio.TaskGroup для структурированной отмены, либо очередь с фиксированным числом воркеров.

Ключевые моменты

  • Семафор, а не нарезка. Деление на пачки по 10 с ожиданием каждой — медленнее: семафор держит ровно 10 активных всегда.
  • return_exceptions=True. Без него первое исключение всплывает из gather, остальные результаты теряются; с ним ошибки — обычные элементы списка.
  • Общий клиент. AsyncClient в async with снаружи всех задач: пул соединений, DNS-кэш, корректное закрытие.
  • Таймауты и отмена. httpx.Timeout на запрос и asyncio.timeout/TaskGroup сверху защищают от вечно висящих задач.

Практический контекст

Классическая senior-задача на секции по конкурентности: краулеры, массовые проверки вебхуков, прогрев кэшей, интеграции с внешними API под rate limit. Интервьюер смотрит, догадается ли кандидат ограничить параллелизм и обработать частичные сбои, а не просто вызвать gather. Разговор обычно продолжается про ретраи, rate limiting по времени и отличие семафора от пула воркеров.

Пример кода

import asyncio, httpx

async def fetch(sem, client, url):
    async with sem:
        r = await client.get(url, timeout=10.0)
        r.raise_for_status()
        return url, len(r.content)

async def main(urls):
    sem = asyncio.Semaphore(10)
    async with httpx.AsyncClient() as client:
        results = await asyncio.gather(
            *(fetch(sem, client, u) for u in urls),
            return_exceptions=True,
        )
    ok = [r for r in results if not isinstance(r, Exception)]
    failed = [r for r in results if isinstance(r, Exception)]
    print(f"ok={len(ok)} failed={len(failed)}")

asyncio.run(main([f"https://example.com/{i}" for i in range(100)]))

Частые ошибки

  • Запускают все 100 запросов без лимита и получают бан или исчерпание сокетов
  • Забывают return_exceptions=True, и одна ошибка прячет 99 успешных ответов
  • Создают новый HTTP-клиент в каждой корутине вместо общего с пулом соединений

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru