← все задачи
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 в потоках: там на сто потоков уходят мегабайты стека, здесь — сто дешёвых задач
Следующая задача
Ограничить число одновременных запросов — Продолжение предыдущей задачи: как сделать параллельно, но не сто сразу.