【问题标题】:Multiple async requests simultaneously同时多个异步请求
【发布时间】:2018-10-27 11:29:20
【问题描述】:

我正在尝试同时调用 ~ 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 关键字的字面意思是等待一个结果。它之后的任何指令仅在结果准备好后执行。并发工作需要同时运行几个协程,而不是一个做几件事。

标签: python python-3.x python-requests python-asyncio


【解决方案1】:

aiohttp 与原生协程 (async/await)

这是一个典型的模式,可以完成你想要做的事情。 (Python 3.7+。)

一个重大变化是您需要从为同步 IO 构建的 requests 迁移到专门为与 async/await 一起使用而构建的包,例如 aiohttp(本机协程) ):

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 和两个awaitables:第一个是.request,第二个是.json。这是异步方面。 await 处理这些 IO 绑定响应的目的是告诉事件循环,其他 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 in async-await function 或 large numbers of tasks with limited concurrency。

【讨论】:

  • 好答案。我希望有一篇博文能简洁地解释这一点。有关该主题的介绍性文本通常已过时或完全不正确。您可能要提到使用Semaphore 来限制并发请求的数量,这往往属于同一模式。
  • @user4815162342 这是我最近写的一篇文章 - 感谢任何反馈和更正。 realpython.com/async-io-python
  • 当我尝试这个时,它会依次完成所有操作(即拨打电话并立即获得该电话的响应),直到我将asyncio.sleep(0) 放在get 之后。这有必要吗?
  • 很棒的文章和答案,@BradSolomon!自 2019 年以来是否有任何更新?
猜你喜欢
  • 2012-05-11
  • 2019-01-05
  • 2015-01-17
  • 2012-05-26
  • 2018-04-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多