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

时间:2018-10-12 16:29:38

标签: python async-await python-asyncio

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

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()吗?如果没有,为什么?

3 个答案:

答案 0 :(得分:9)

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

关于第一个问题,这在Python中当然是可以实现的,就像Chrome Canary Dev Tools中可以实现的那样-通过工具通过自己与事件循环的集成来处理它。确实,IPython 7.0和更高版本支持异步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

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

>>> 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实现,该实现与我们的小型事件循环配合使用。为此,我们需要使用迭代器来实现awaitable:

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事件循环以及许多其他功能带来的结果。 this talk的大卫·比兹利(David Beazley)充分展示了从头开始构建事件循环,他在现场观众面前实现了功能性事件循环。

答案 1 :(得分:3)

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

但是,您可以执行一些协程,而无需将其显式传递给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())

答案 2 :(得分:3)

因此,经过一番挖掘,我认为我找到了在全局范围内执行协程的最简单解决方案。

如果您>>> 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'

但是,当None有待等待的东西时,它返回coro(这实际上是协程的本质)。

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