如何在事件循环之外运行协同程序?

2024-10-01 09:16:02 发布

您现在位置:Python中文网/ 问答频道 /正文

因此,通常情况下,您可以通过执行以下操作来获取协同程序的结果:

async def coro():
    await asycnio.sleep(3)
    return 'a value'

loop = asyncio.get_event_loop()
value = loop.run_until_complete(coro())

出于好奇,在不使用事件循环的情况下,最简单的方法是什么?在

[编辑]

我认为更简单的方法是:

^{pr2}$

但是,有没有什么方法可以像JS那样对yield from(或await)进行全局排序呢?如果没有,为什么?在


Tags: 方法loopeventasynciogetasyncreturnvalue
3条回答

这里有两个问题:一个是关于“在顶层”或更具体地说,在开发环境中等待协同程序。另一个是运行一个没有事件循环的协同程序。在

关于第一个问题,这在Python中当然是可能的,就像在Chrome Canary Dev Tools中一样-通过工具自己与事件循环的集成来处理它。实际上,ipython7.0及更高版本支持asyncia natively,您可以按照预期在顶层使用await coro()。在

关于第二个问题,在没有事件循环的情况下驱动一个协程是很容易的,但是它不是很有用。让我们来看看为什么。在

调用协程函数时,它返回一个协程对象。此对象通过调用其send()方法来启动和恢复。当协同程序决定挂起(因为它await是阻塞的东西),send()将返回。当协同程序决定返回时(因为它已经到达结尾或者因为它遇到了一个显式的return),它将引发一个StopIteration异常,并将value属性设置为返回值。考虑到这一点,一次合作路线的最小驱动程序可能如下所示:

def drive(c):
    while True:
        try:
            c.send(None)
        except StopIteration as e:
            return e.value

这对于简单的协同工作非常有效:

^{pr2}$

或者更复杂的一点:

>>> async def plus(a, b):
...     return a + b
... 
>>> async def pi():
...     val = await plus(3, 0.14)
...     return val
... 
>>> drive(pi())
3.14

但是仍然缺少一些东西——上面的协同程序都没有暂停执行。当一个协程暂停时,它允许其他协程运行,这使得事件循环能够(看起来)一次执行多个协程。例如,asyncio有一个sleep()协程,当等待时,它将在指定的时间段内暂停执行:

async def wait(s):
    await asyncio.sleep(1)
    return s

>>> asyncio.run(wait("hello world"))
'hello world'      # printed after a 1-second pause

但是,drive无法执行此协同程序以完成:

>>> drive(wait("hello world"))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in drive
  File "<stdin>", line 2, in wait
  File "/usr/lib/python3.7/asyncio/tasks.py", line 564, in sleep
    return await future
RuntimeError: await wasn't used with future

实际情况是,sleep()通过生成一个特殊的“future”对象与事件循环通信。等待未来的合作路线只能在未来确定后才能恢复。“真正的”事件循环将通过运行其他协程来实现,直到未来完成。在

为了解决这个问题,我们可以编写自己的sleep实现,它与我们的小事件循环一起工作。为此,我们需要使用迭代器来实现waitable:

class my_sleep:
    def __init__(self, d):
        self.d = d
    def __await__(self):
        yield 'sleep', self.d

我们生成一个元组,协同程序调用者看不到它,但它会告诉drive(我们的事件循环)该怎么做。drivewait现在如下所示:

def drive(c):
    while True:
        try:
            susp_val = c.send(None)
            if susp_val is not None and susp_val[0] == 'sleep':
                time.sleep(susp_val[1])
        except StopIteration as e:
            return e.value

async def wait(s):
    await my_sleep(1)
    return s

使用此版本,wait可以正常工作:

>>> drive(wait("hello world"))
'hello world'

这仍然不是很有用,因为驱动协同程序的唯一方法是调用drive(),它同样支持单个协同例程。因此,我们不妨编写一个同步函数,简单地调用time.sleep()并调用一天。为了支持异步编程的用例,drive()需要:

  • 支持多个协同路由的运行和暂停
  • 在驱动器循环中实现新协同程序的生成
  • 允许协同程序在IO相关事件上注册唤醒,例如文件描述符变得可读或可写—同时支持多个这样的事件而不损失性能

这就是asyncio事件循环带来的结果,以及许多其他特性。davidbeazley在this talk中出色地演示了从头开始构建事件循环,他在现场观众面前实现了一个功能性事件循环。在

所以,经过一番研究,我想我找到了在全球范围内执行协同程序的最简单的解决方案。在

如果>>> dir(coro)Python将打印出以下属性:

['__await__', '__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'cr_await', 'cr_code', 'cr_frame', 'cr_origin', 'cr_running', 'send', 'throw']

有几个属性非常突出,即:

^{pr2}$

在阅读了what does yield (yield) do?以及生成器的工作原理之后,我认为send方法一定是关键。在

所以我试着:

>>> the_actual_coro = coro()
<coroutine object coro at 0x7f5afaf55348> 

>>>the_actual_coro.send(None)

它引发了一个有趣的错误:

Original exception was:
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
StopIteration: a value

它实际上给了我一个异常的返回值!在

所以我认为一个非常基本的循环,嗯,它更像是一个运行器,可以这样实现:

def run(coro):
    try:
        coro.send(None)
    except StopIteration as e:
        return e.value

现在,我可以在同步函数中运行协同程序,甚至可以全局运行,但我不建议这样做。但是,了解运行协同程序的最简单和最低级别是很有趣的

>>> run(coro())
'a value'

然而,当coro有一些事情要等待时,它将返回None(这实际上是作为一个协程的本质)。在

我认为这可能是因为事件循环通过将它们分配给futures并分别处理它们来处理它的协程(coro.cr_frame.f_locals)的等待项?我的简单run函数显然没有提供。在这方面我可能错了。所以如果我错了,请有人纠正我。在

如果不使用事件循环,就无法获得协程的值,因为协程只能由事件循环执行。在

但是,您可以执行一些协同程序而不显式地将其传递给run_until_complete。您可以在事件循环运行时等待它获取值。例如:

import asyncio


async def test():
    await asyncio.sleep(1)
    return 'a value'


async def main():
    res = await test()
    print('got value from test() without passing it to EL explicitly')
    print(res)


if __name__ ==  '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

相关问题 更多 >