Python asyncio ensure_future 装饰器

Posted

技术标签:

【中文标题】Python asyncio ensure_future 装饰器【英文标题】:Python asyncio ensure_future decorator 【发布时间】:2017-12-18 21:05:05 【问题描述】:

假设我是 asyncio 的新手。我正在使用 async/await 来并行化我当前的项目,并且我发现自己将所有协程都传递给了asyncio.ensure_future。很多这样的东西:

coroutine = my_async_fn(*args, **kwargs)
task = asyncio.ensure_future(coroutine)

我真正想要的是调用一个异步函数来返回一个正在执行的任务,而不是一个空闲的协程。我创建了一个装饰器来完成我想要做的事情。

def make_task(fn):
    def wrapper(*args, **kwargs):
        return asyncio.ensure_future(fn(*args, **kwargs))
    return wrapper

@make_task
async def my_async_func(*args, **kwargs):
    # usually making a request of some sort
    pass

asyncio 是否有一种我无法找到的内置方法?如果我一开始就导致这个问题,我是否使用了错误的 asyncio?

【问题讨论】:

你的装饰器包装被阻塞了。 'def wrapper' 应该是 'async def wrapper' 以不阻塞 我可能不明白。自从我用 Python 做很多事情以来已经有一段时间了。但我想我想同步创建一个异步任务。如果我做到了async def wrapper,我的包装函数不会创建一个在执行时会创建任务的协程吗? 完全正确,所以也许我错过了理解你的意思......我发现你的问题是完全按照你所说的去做,所以我不得不在我的包装器前面添加异步字案子。无论如何,出于您的问题目的,没有“异步”语法就是这样! 【参考方案1】:

asyncio 在很早的预发布版本中有 @task 装饰器,但我们删除了它。

原因是装饰器不知道使用什么循环。 asyncio 不会在导入时实例化循环,而且为了测试隔离,测试套件通常会为每个测试创建一个新循环。

【讨论】:

好的,这很有趣。谢谢。【参考方案2】:

asyncio 有内置的方法吗?我无法做到 找到了吗?

不,asyncio 没有将协程函数转换为任务的装饰器。

如果我一开始就导致这个问题,我是否使用了错误的 asyncio?

没有看到你在做什么很难说,但我认为这可能是真的。虽然创建任务是异步程序中的常见操作,但我怀疑您创建了这么多应该始终是任务的协程。

等待协程 - 是一种“异步调用某些函数”的方式,但会阻塞当前的执行流程直到它完成:

await some()

# you'll reach this line *only* when some() done 

另一方面,任务是“运行函数in background”的一种方式,它不会阻塞当前的执行流程:

task = asyncio.ensure_future(some())

# you'll reach this line immediately

当我们编写asyncio程序时,我们通常需要第一种方式,因为我们通常需要一些操作的结果才能开始下一个:

text = await request(url)

links = parse_links(text)  # we need to reach this line only when we got 'text'

另一方面,创建任务通常意味着后续代码不依赖于任务的结果。但同样,它并不总是发生。

由于ensure_future 立即返回,因此有些人试图将其用作同时运行某些协程的一种方式:

# wrong way to run concurrently:
asyncio.ensure_future(request(url1))
asyncio.ensure_future(request(url2))
asyncio.ensure_future(request(url3))

实现这一点的正确方法是使用asyncio.gather:

# correct way to run concurrently:
await asyncio.gather(
    request(url1),
    request(url2),
    request(url3),
)

这就是你想要的吗?

更新:

我认为在您的情况下使用任务是个好主意。但我认为您不应该使用装饰器:协程功能(发出请求)仍然是与其具体使用细节分开的部分(它将用作任务)。如果请求同步控制与其主要功能是分开的,那么将同步移动到单独的功能中也是有意义的。我会这样做:

import asyncio


async def request(i):
    print(f'i started')
    await asyncio.sleep(i)
    print(f'i finished')
    return i


async def when_ready(conditions, coro_to_start):
    await asyncio.gather(*conditions, return_exceptions=True)
    return await coro_to_start


async def main():
    t = asyncio.ensure_future

    t1 = t(request(1))
    t2 = t(request(2))
    t3 = t(request(3))
    t4 = t(when_ready([t1, t2], request(4)))
    t5 = t(when_ready([t2, t3], request(5)))

    await asyncio.gather(t1, t2, t3, t4, t5)


if __name__ ==  '__main__':
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.run_until_complete(loop.shutdown_asyncgens())
        loop.close()

【讨论】:

谢谢,我知道并正在使用asyncio.gather,但协程最大的问题之一是它们只能等待一次。我想做的更像是这样:发送 3 个请求;当请求 1 和 2 完成时,发送请求 4;当请求 2 和 3 完成时,发送请求 5。我只是将任务 1 和 2 发送到任务 4 并在那里收集它们,并在任务 5 中收集任务 2 和 3。有没有办法在没有任务的情况下做类似的事情?我不想在请求 5 发送之前等待请求 1 完成,或者在请求 4 发送之前等待请求 3 完成。 嗯,这是一个有趣的方法。不过,我只需要在最后收集 t4 和 t5,对吧? @BrettBeatty 是的,如果你在最后一次收集中只留下 t4 和 t5,这个程序将以相同的方式工作。做得更好取决于脚本的最终目标听起来如何:是仅请求 t4 和 t5(可能取决于其他请求,如 1-3)还是它,例如,所有五个请求(应该同步以一种特殊的方式)。

以上是关于Python asyncio ensure_future 装饰器的主要内容,如果未能解决你的问题,请参考以下文章

深究Python中的asyncio库-asyncio简介与关键字

Python协程之asyncio

python:asyncio模块

Python asyncio 模块

python asyncio

Python标准模块--asyncio