【问题标题】:How to run a coroutine outside of an event loop?如何在事件循环之外运行协程?
【发布时间】:2023-03-19 09:55:02
【问题描述】:

通常,您可以通过执行以下操作来获取协程的结果:

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

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

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

[编辑]

我认为更简单的方法是:

async def coro():
    ...

value = asyncio.run(coro())  # Python 3.7+

但是有什么方法可以像JS 一样在全局范围内对yield from(或await)和coro() 进行排序?如果不是,为什么?

【问题讨论】:

  • 好吧,你总是可以使用一个不打算在asyncio的上下文中使用的协程,即你使用got_value = yield producing_value语法手动发送值的协程。或者您是在严格谈论利用新 async/await 语法的协程?
  • 严格意义上的async/await 语法。不过感谢您的建议。

标签: python async-await python-asyncio


【解决方案1】:

所以经过一番挖掘,我想我找到了全局执行协程的最简单解决方案。

如果你>>> 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']

有几个属性很突出,即:

[
   '__await__',
   'close',
   'cr_await',
   'cr_code',
   'cr_frame',
   'cr_origin',
   'cr_running',
   'send',
   'throw'
]

在阅读了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(这确实是协程的本质)。

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

【讨论】:

    【解决方案2】:

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

    关于第一个问题,这在 Python 中肯定是可能的,就像在 Chrome Canary 开发工具中可能一样 - 通过工具通过自己与事件循环的集成来处理它。事实上,IPython 7.0 及更高版本支持 asyncio natively,您可以按预期在顶层使用 await coro()。

    关于第二个问题,没有事件循环的情况下,驱动单个协程很容易,但用处不大。让我们来看看原因。

    当调用协程函数时,它返回一个协程对象。该对象通过调用其send() 方法来启动和恢复。当协程决定 suspend 时(因为它awaits 会阻塞),send() 将返回。当协程决定返回时(因为它已经到了结尾或者因为它遇到了一个显式的return),它将引发一个StopIteration异常,并将value属性设置为返回值.考虑到这一点,单个协程的最小驱动程序可能如下所示:

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

    这对于简单的协程非常有用:

    >>> async def pi():
    ...     return 3.14
    ... 
    >>> drive(pi())
    3.14
    

    或者更复杂一点的:

    >>> 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() 通过产生一个特殊的“未来”对象与事件循环通信。等待未来的协程只能在设置未来后恢复。 “真正的”事件循环会通过运行其他协程来完成,直到未来完成。

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

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

    我们生成一个不会被协程调用者看到的元组,但会告诉drive(我们的事件循环)该做什么。 drive 和 wait 现在看起来像这样:

    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 事件循环带来的,以及许多其他功能。 David Beazley 在this talk 中出色地演示了从头开始构建事件循环,他在现场观众面前实现了功能性事件循环。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-02-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-05-19
      • 1970-01-01
      • 2015-08-08
      • 2018-07-20
      相关资源
      最近更新 更多