首页 > 解决方案 > 如何用等效的 asyncio 表达式替换 `yield gen.Task(fn, argument)`?

问题描述

问题: 我正在尝试更新一些旧代码(不是我编写的),它使用过时版本的 Tornado 和gen.Task,以使用当前版本的 Tornado 和 asyncio。这很简单,除了(1)我不完全理解的一个表达式,以及(2)我不知道如何用等效的 asyncio 表达式替换。

我要替换的单行代码是以下形式:

response = yield gen.Task(fn, request)

函数的签名在哪里fnfn(request, callback)然后在gen.coroutine我们运行的代码(这是 a 的方法定义)中callback(response)。而且我认为fn它本身可能是异步的,尽管我不确定,也不明白如果它是真的会产生什么影响。

编辑:根据另一个答案的建议,我能够将其重写为

fn(request=request, callback=(yield gen.Callback("key")))
response = yield gen.Wait("key")

当然,尽管 Tornado 6 的发行说明说两者gen.Wait都已gen.Callback被删除。早期版本的文档说gen.Wait已弃用,应替换为tornado.concurrent.Futures,不幸的是,它没有指定如何这样做,特别是考虑到如何gen.Wait需要key参数,而concurrent.futures.Futures(显然是 的别名asyncio.Future)明确无法支持key参数。所以我不明白这是可以以某种方式替换的说法。

add_done_callback似乎也不适用于此目的,因为文档明确指出回调只能接受一个参数,但有fn两个。

尽管到目前为止效果最好(并且可能实际上有效,只要我可以在其他地方gen.coroutine正确async def过渡)似乎是:

response = await asyncio.Future().add_done_callback(partial(fn, request=request))

gen.coroutine这只会产生意想不到的行为(无休止的阻塞,似乎可能是因为上面提到的转换不足async def)并且没有错误这给出了错误TypeError: Can't await NoneType。所以我没有头绪。

背景:我试图弄清楚 Tornado 在gen.Task更新和最终删除时给出了哪些建议。然而,在版本 6 的更新日志中,它没有说明如何使用 更新我们的代码gen.Task,只是说它已被删除。我在 StackOverflow和Tornado GitHub 问题上发现了至少一个问题,据说(没有给出具体示例或实现细节)任何实例都可以替换为. 但是,由于我不太了解异步编程的一般概念,也不了解异步编程的细节,因此很难弄清楚如何做到这一点。不过它会很棒,因为它似乎很容易更换gen.Taskgen.coroutinetornado.gen.Taskgen.coroutine's with asyncio 等价物——只是async defawait一切。

根据文档,结果yield gen.Task应该是:

接受一个函数(和可选的附加参数)并使用这些参数加上一个回调关键字参数运行它。传递给回调的参数作为 yield 表达式的结果返回。

在 4.0 版更改:gen.Task现在是一个返回Future...

然而,这似乎比可以替换的东西更复杂gen.coroutine,因为它直接创建 a Future,而不是awaiting 异步函数的结果,并且有很多方法可以在 asyncio 中创建和使用期货,我隐约记得在某处读过Tornado 期货和 asyncio 期货实际上并不等同。

这里面同时涉及到异步和函数式编程,让这个问题更难理解——函数式的部分我还隐约掌握,但是我对异步编程的理解很差,以至于一下子也让函数式的部分变得难以理解现在也明白了。


到目前为止我已经尝试过:

response = yield asyncio.add_done_callback(functools.partial(fn, request=request))

给出错误AttributeError: module 'asyncio' has no attribute 'add_done_callback',很好,我知道它add_done_callback应该是一个asyncio.Future对象的属性,但是我应该做什么/选择是asyncio.Future什么?

response = yield asyncio.Task(partial(fn, request=request).func)

这给出了错误TypeError: a coroutine was expected, got <bound method Class.fn of <SubClass object at 0x7f5df254b748>>

我尝试使用.func部分对象的属性的原因是因为当我尝试时:

response = yield asyncio.Task(partial(fn, request=request))

我得到了错误TypeError: a coroutine was expected, got functools.partial(<bound method Class.fn of <SubClass object at 0x7ffaad59b710>>, request=<tornado.httpclient._RequestProxy object at 0x7ffaad4c8080>)。但我只是尝试这样做,因为更直接的解决方案尝试导致抱怨参数数量不正确。

尤其是尝试最天真的事情之一,

response = yield asyncio.Task(fn, request)

导致了事后可预见的错误TypeError: Task() takes at most 1 positional arguments (2 given)Tornado 5.0 的发行说明说内部所有gen.Task的 ' 都被替换为asyncio.Task',但这让我很难理解如何,因为它asyncio.Task本身似乎不足以处理回调。

我本来比较乐观,希望asyncio.Task能注意到 is 的调用签名,fn然后fn(request, callback)理解fn(request)为部分应用的函数。但那当然

response = yield asyncio.Task(fn(request))

给出了错误TypeError: fn() missing 1 required positional argument: 'callback'

更令人困惑的是它fn本身可能是异步的,所以我认为使用asyncio我可能只能部分应用它并取回一个以回调为选项的异步函数

response = yield fn(request)

但这只是导致了错误TypeError: fn() missing 1 required positional argument: 'callback'

ensure_future我还尝试使用推荐的和create_task函数在 asyncio 中创建任务或未来(我不确定我需要创建两个中的哪一个) ,因为Task根据 asyncio 文档,强烈建议不要直接使用。这并没有很好地解决:

response = yield asyncio.create_task(fn, request)

给出错误TypeError: create_task() takes 1 positional argument but 2 were given

使用ensure_future导致没有更好的结果:

response = asyncio.ensure_future(functools.partial(fn, request))

给出了结果TypeError: An asyncio.Future, a coroutine or an awaitable is required,而不是使用partial

response = asyncio.ensure_future(super().fetch_impl, request=request)

给出了错误 TypeError: ensure_future() got an unexpected keyword argument 'request'

如果它是相关的,fnfetch_implTornado 的方法CurlAsyncHTTPClient

类似的问题:这两个问题看起来很相似,但我不明白如何使用他们的答案来解决我的问题。它们可能是适用的,但是我对一般异步编程和特别是异步编程的理解非常糟糕,而且我非常愚蠢。因此,我也将不胜感激解释其他两个问题的答案的答案,例如我五岁。您对我的愚蠢+无知的任何耐心将不胜感激。

进行异步调用时,“yield”在龙卷风中如何工作?

扩展 tornado.gen.Task

标签: asynchronouscallbacktornadopython-asynciocoroutine

解决方案


我在 StackOverflow 和 Tornado GitHub 问题上发现了至少一个问题,据说(没有给出具体示例或实现细节)任何 gen.Task 实例都可以替换为 gen.coroutine。但是,由于我不太了解异步编程的一般概念,也不了解 tornado.gen.Task 的细节,因此很难弄清楚如何做到这一点。不过这会很棒,因为用 asyncio 等价物替换 gen.coroutine 似乎很容易——只需 async def 并等待一切。

您专注于“我如何称呼这个需要回调的东西”。问题是回调的整个概念已被弃用并从 Tornado 中删除,因此不再有优雅的方式来调用需要回调的东西。预期的前进路径是更改需要回调的事物(即fn)以使用gen.coroutine和/或返回 a Future,然后您可以直接从其他协程调用它。

如果fn使用的是@gen.engine(Tornado 中协同程序的第一个版本),这相当简单:只需替换@gen.engine@gen.coroutine删除对callback参数的任何引用。该函数可能以callback(response);结尾 将此替换为raise gen.Return(response).

如果fn只是在没有 的情况下使用原始回调@gen.engine,那么将其更新为以现代方式工作将更加困难,并且需要根据具体情况进行处理,因此我无法在此处为您提供有用的指导。

如果您遇到需要回调的东西并且无法更改它,则此序列几乎等同于response = yield gen.Task(fn, request)

future = tornado.concurrent.Future()
fn(request, callback=future.set_result)
response = yield future

这之间的区别gen.Task与错误处理有关。如果fn引发异常,gen.Task则需要一些昂贵的魔法来确保它可以捕获该异常并在调用函数中重新引发它。即使对于不使用 的应用程序,保持这种魔力也会带来一些性能成本gen.Task,这就是为什么它最终被弃用和删除(以及与回调相关的所有内容)。因此,您可能需要进行更改fn以确保适当地捕获和报告任何可能的异常(再次,推荐的方法是转移到异常处理更符合您预期的协同程序)。


推荐阅读