一旦所有HTTP请求返回后,我想启动大量HTTP请求并收集其结果。 asyncio
可以以非阻塞方式发送请求,但是我在收集其结果时遇到问题。
我知道针对此特定问题的解决方案,例如aiohttp。但是HTTP请求只是一个示例,我的问题是如何正确使用asyncio
。
在服务器端,我有flask可以用“ Hello World!”回答localhost/
的每个请求,但是要等待0.1秒再回答。在所有示例中,我正在发送10个请求。同步代码大约需要1秒钟,异步版本可以在0.1秒内完成。
在客户端,我想同时启动许多请求并收集其结果。我正在尝试以三种不同的方式来做到这一点。由于asyncio需要执行者来解决阻塞代码,因此所有方法都调用loop.run_in_executor
。
此代码在他们之间共享:
import requests
from time import perf_counter
import asyncio
loop = asyncio.get_event_loop()
async def request_async():
r = requests.get("http://127.0.0.1:5000/")
return r.text
def request_sync():
r = requests.get("http://127.0.0.1:5000/")
return r.text
方法1:
在任务列表上使用asyncio.gather()
,然后使用run_until_complete
。读完Asyncio.gather vs asyncio.wait后,似乎collect会等待结果。但事实并非如此。因此,此代码立即返回,而无需等待请求完成。
如果我在此处使用阻止功能,则可以正常工作。为什么我不能使用异步功能?
# approach 1
start = perf_counter()
tasks = []
for i in range(10):
tasks.append(loop.run_in_executor(None, request_async)) # <---- using async function !
gathered_tasks = asyncio.gather(*tasks)
results = loop.run_until_complete(gathered_tasks)
stop = perf_counter()
print(f"finished {stop - start}") # 0.003
# approach 1(B)
start = perf_counter()
tasks = []
for i in range(10):
tasks.append(loop.run_in_executor(None, request_sync)) # <---- using sync function
gathered_tasks = asyncio.gather(*tasks)
results = loop.run_until_complete(gathered_tasks)
stop = perf_counter()
print(f"finished {stop - start}") # 0.112
Python甚至警告我coroutine "request_async"
从未等待。
在这一点上,我有一个可行的解决方案:在执行程序中使用常规(而非异步)功能。但是我想有一个可以与async
函数定义一起使用的解决方案。因为我想在其中使用await
(在这个简单的示例中这不是必需的,但是如果我将更多代码移到asyncio
上,我相信它将变得很重要)。
方法2:
Python警告我,从未等待过协程。因此,让我们等待他们。方法2将所有代码包装到外部异步函数中,并等待收集的结果。同样的问题,也立即返回(也同样警告):
# approach 2
async def main():
tasks = []
for i in range(10):
tasks.append(loop.run_in_executor(None, request_async))
gathered_tasks = asyncio.gather(*tasks)
return await gathered_tasks # <-------- here I'm waiting on the coroutine
start = perf_counter()
results = loop.run_until_complete(main())
stop = perf_counter()
print(f"finished {stop - start}") # 0.0036
这真的让我感到困惑。我正在等待gather
的结果。直观地讲,应该传播到我正在收集的协程中。但是python仍然抱怨我的协程从未等待过。
我阅读了更多内容,发现:How could I use requests in asyncio?
这几乎就是我的示例:结合requests
和asyncio
。这使我进入方法3:
方法3:
结构与方法2相同,但要分别等待分别交给run_in_executor()
的每个任务(当然,这要等到等待协程):
# approach 3:
# wrapping executor in coroutine
# awaiting every task individually
async def main():
tasks = []
for i in range(10):
task = loop.run_in_executor(None, request_async)
tasks.append(task)
responses = []
for task in tasks:
response = await task
responses.append(response)
return responses
start = perf_counter()
results = loop.run_until_complete(main())
stop = perf_counter()
print(f"finished {stop - start}") # 0.004578
我的问题是:我想在协程中包含阻塞代码,并与执行程序并行运行它们。我如何得到他们的结果?
答案 0 :(得分:3)
我的问题是:我想在协程中包含阻塞代码,并与执行程序并行运行它们。我如何得到他们的结果?
答案是,协程中不应包含阻塞代码。如果必须使用它,则必须使用run_in_executor
将其隔离。因此,写request_async
(使用requests
)的唯一正确方法是:
async def request_async():
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, request_sync)
将request_async
赋予run_in_executor
是注定的,因为run_in_executor
的整个 point 要在另一个调用 sync 函数线。如果给它提供协程函数,它将很高兴地调用它(在另一个线程中),并将返回的协程对象作为“结果”提供。这等效于将生成器传递给需要普通函数的代码-是的,它将调用生成器就好了,但是它不知道如何处理返回的对象。
更重要的是,您不能仅将async
放在def
前面并期望获得可用的协程。协程一定不能阻塞,除非等待其他异步代码。
现在,一旦有了可用的request_async
,就可以像这样收集其结果:
async def main():
tasks = [request_async() for _i in range(10)]
results = await asyncio.gather(*tasks)
return results
results = loop.run_until_complete(main())