Несколько асинхронных запросов одновременно

Я пытаюсь вызвать ~ 300 вызовов API одновременно, чтобы получить результат максимум за пару секунд.

Мой псевдокод выглядит так:

def function_1():
    colors = ['yellow', 'green', 'blue', + ~300 other ones]
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    res = loop.run_until_complete(get_color_info(colors))

async def get_color_info(colors):
    loop = asyncio.get_event_loop()
    responses = []
    for color in colors:
        print("getting color")
        url = "https://api.com/{}/".format(color)
        data = loop.run_in_executor(None, requests.get, url)
        r = await data
        responses.append(r.json())
    return responses

Делая это, я распечатываю getting color каждую секунду или около того, а код занимает вечность, поэтому я почти уверен, что они не запускаются одновременно. Что я делаю неправильно?

Ключевое слово await буквально означает ждать результат. Любые инструкции после его выполнения только после результат готов. Для одновременного выполнения работы требуется одновременный запуск сопрограмм несколько, а не выполнение одной из нескольких задач.

MisterMiyagi 28.10.2018 21:19
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
13
1
18 184
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

aiohttp с родными сопрограммами (async / await)

Вот типичный паттерн, который выполняет то, что вы пытаетесь сделать. (Python 3.7+.)

Одним из основных изменений является то, что вам нужно будет перейти от requests, который создан для синхронного ввода-вывода, к пакету, подобному aiohttp, который создан специально для работы с async / await (собственные сопрограммы):

import asyncio
import aiohttp  # pip install aiohttp aiodns


async def get(
    session: aiohttp.ClientSession,
    color: str,
    **kwargs
) -> dict:
    url = f"https://api.com/{color}/"
    print(f"Requesting {url}")
    resp = await session.request('GET', url=url, **kwargs)
    # Note that this may raise an exception for non-2xx responses
    # You can either handle that here, or pass the exception through
    data = await resp.json()
    print(f"Received data for {url}")
    return data


async def main(colors, **kwargs):
    # Asynchronous context manager.  Prefer this rather
    # than using a different session for each GET request
    async with aiohttp.ClientSession() as session:
        tasks = []
        for c in colors:
            tasks.append(get(session=session, color=c, **kwargs))
        # asyncio.gather() will wait on the entire task set to be
        # completed.  If you want to process results greedily as they come in,
        # loop over asyncio.as_completed()
        htmls = await asyncio.gather(*tasks, return_exceptions=True)
        return htmls


if __name__ == '__main__':
    colors = ['red', 'blue', 'green']  # ...
    # Either take colors from stdin or make some default here
    asyncio.run(main(colors))  # Python 3.7+

В этом есть два различных элемента, один из которых является асинхронным аспектом сопрограмм, а другой - параллелизмом, вводимым поверх этого, когда вы указываете контейнер задач (фьючерсы):

  • Вы создаете одну сопрограмму get, которая использует await с двумя ожидаемый: первая - .request, вторая - .json. Это асинхронный аспект. Цель await, отправляющего эти связанные с вводом-выводом ответы, - сообщить циклу обработки событий, что другие вызовы get() могут по очереди выполнять ту же процедуру.
  • Параллельный аспект инкапсулирован в await asyncio.gather(*tasks). Это отображает ожидаемый вызов get() на каждый из ваших colors. Результатом является совокупный список возвращаемых значений. Обратите внимание, что эта оболочка будет ждать, пока не придет все ваших ответов, и вызовет .json(). Если, в качестве альтернативы, вы хотите обработать их с жадностью, когда они готовы, вы можете перебрать asyncio.as_completed: каждый возвращенный объект Future представляет самый ранний результат из набора оставшихся ожидаемых.

Наконец, обратите внимание, что asyncio.run() - это высокоуровневая "фарфоровая" функция, представленная в Python 3.7. В более ранних версиях вы можете имитировать это (примерно) следующим образом:

# The "full" versions makes a new event loop and calls
# loop.shutdown_asyncgens(), see link above
loop = asyncio.get_event_loop()
try:
    loop.run_until_complete(main(colors))
finally:
    loop.close()

Ограничение запросов

Есть несколько способов ограничить скорость параллелизма. Например, см. asyncio.semaphore в функции async-await или большое количество задач с ограниченным параллелизмом.

Хороший ответ. Хотел бы я, чтобы в блоге это объяснялось так лаконично. Вступительные тексты по теме часто устарели или совершенно неверны. Возможно, вы захотите упомянуть об использовании Semaphore для ограничения количества одновременных запросов, которые, как правило, принадлежат одному и тому же шаблону.

user4815162342 27.10.2018 22:58

@ user4815162342 Вот статья, которую я недавно написал - благодарю за любые отзывы и исправления. realpython.com/async-io-python

Brad Solomon 31.01.2019 15:04

когда я пробовал это, он до тех пор, пока не делал все последовательно (то есть звонил и сразу получал ответ для этого звонка), пока я не поставил asyncio.sleep(0) после get. это необходимо?

dstandish 11.12.2020 00:37

Другие вопросы по теме