Я пытаюсь вызвать ~ 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
каждую секунду или около того, а код занимает вечность, поэтому я почти уверен, что они не запускаются одновременно. Что я делаю неправильно?
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 Вот статья, которую я недавно написал - благодарю за любые отзывы и исправления. realpython.com/async-io-python
когда я пробовал это, он до тех пор, пока не делал все последовательно (то есть звонил и сразу получал ответ для этого звонка), пока я не поставил asyncio.sleep(0)
после get
. это необходимо?
Ключевое слово
await
буквально означает ждать результат. Любые инструкции после его выполнения только после результат готов. Для одновременного выполнения работы требуется одновременный запуск сопрограмм несколько, а не выполнение одной из нескольких задач.