asyncio:从执行程序中的异步函数收集结果

时间:2018-11-12 13:50:21

标签: python asynchronous async-await python-asyncio coroutine

一旦所有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?

这几乎就是我的示例:结合requestsasyncio。这使我进入方法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

我的问题是:我想在协程中包含阻塞代码,并与执行程序并行运行它们。我如何得到他们的结果?

1 个答案:

答案 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())