← все задачи

Async Python · задача 1 из 10

Забрать сто URL параллельно

Начальный 15 минут gatherконкурентностьклиентская сессия

Условие

Дан список из ста URL. Заберите их все и верните список ответов. Запросы должны идти параллельно, а не один за другим.

Что требуется

  • Результаты в том же порядке, что и входные URL
  • Один HTTP-клиент на все запросы, а не новый на каждый
  • Общее время близко к времени самого медленного запроса, а не к сумме

Пример

urls = ["https://api.example.com/1", ...]

results = await fetch_all(urls)
# 100 запросов по 200мс: последовательно ~20с, параллельно ~0.3с

Сначала уточните

Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.

  • Что делать, если часть запросов упала: падать целиком или вернуть, что получилось?
  • Есть ли ограничение на число одновременных соединений со стороны чужого API?
  • Нужен ли таймаут на запрос и на всю операцию?
Показать решение Скрыть решение

Решение

import asyncio
import aiohttp


async def fetch_one(session, url):
    async with session.get(url) as response:
        response.raise_for_status()
        return await response.json()


async def fetch_all(urls):
    timeout = aiohttp.ClientTimeout(total=10)

    async with aiohttp.ClientSession(timeout=timeout) as session:
        tasks = [fetch_one(session, url) for url in urls]
        return await asyncio.gather(*tasks)


# Так делать нельзя — это последовательное выполнение:
# for url in urls:
#     results.append(await fetch_one(session, url))

Почему так

Почему цикл с await — это не параллельно

  • await останавливает корутину до получения ответа: следующий запрос уйдёт только после предыдущего
  • Сто запросов по 200 мс превращаются в 20 секунд вместо 0.3 — разница видна на глаз
  • Это главная проверка задачи: отличаете ли вы «написал async» от «сделал конкурентно»

Что именно делает gather

  • gather заворачивает корутины в задачи и отдаёт их циклу событий разом, а результаты собирает в порядке аргументов
  • Порядок результатов не зависит от порядка завершения — это и даёт «в том же порядке, что URL»
  • При исключении в одной задаче gather по умолчанию пробрасывает его, а остальные задачи продолжают работать — отсюда return_exceptions в следующей задаче

Почему одна сессия на все запросы

  • ClientSession держит пул соединений: новая сессия на запрос — это новый TCP и новый TLS-хендшейк каждый раз
  • Создание сессии внутри fetch_one — самая частая ошибка на ревью и стабильная потеря производительности
  • Сессию нужно закрывать, поэтому async with вокруг всей операции

Что спросят дальше

  • Следом попросят ограничить конкурентность — сто одновременных запросов чужой API может не пережить
  • Спросят, что будет при исключении в одном из ста запросов — и ждут разговора про return_exceptions
  • Спросят про requests в потоках: там на сто потоков уходят мегабайты стека, здесь — сто дешёвых задач

Следующая задача

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