如何创建一个事件循环,其中包含滚动协程并永久运行?

22
为了防止上下文切换,我想创建一个大循环来同时服务于网络连接和一些例行程序。
以下是正常函数的实现:
import asyncio
import time


def hello_world(loop):
    print('Hello World')
    loop.call_later(1, hello_world, loop)

def good_evening(loop):
    print('Good Evening')
    loop.call_later(1, good_evening, loop)

print('step: asyncio.get_event_loop()')
loop = asyncio.get_event_loop()

print('step: loop.call_soon(hello_world, loop)')
loop.call_soon(hello_world, loop)
print('step: loop.call_soon(good_evening, loop)')
loop.call_soon(good_evening, loop)

try:
    # Blocking call interrupted by loop.stop()
    print('step: loop.run_forever()')
    loop.run_forever()
except KeyboardInterrupt:
    pass
finally:
    print('step: loop.close()')
    loop.close()

这是协程的实现方式:

import asyncio


@asyncio.coroutine
def hello_world():
    while True:
        yield from asyncio.sleep(1)
        print('Hello World')

@asyncio.coroutine
def good_evening():
    while True:
        yield from asyncio.sleep(1)
        print('Good Evening')

print('step: asyncio.get_event_loop()')
loop = asyncio.get_event_loop()
try:
    print('step: loop.run_until_complete()')
    loop.run_until_complete(asyncio.wait([
        hello_world(),
        good_evening()
    ]))
except KeyboardInterrupt:
    pass
finally:
    print('step: loop.close()')
    loop.close()

还有混合的一种:

import asyncio
import time


def hello_world(loop):
    print('Hello World')
    loop.call_later(1, hello_world, loop)

def good_evening(loop):
    print('Good Evening')
    loop.call_later(1, good_evening, loop)

@asyncio.coroutine
def hello_world_coroutine():
    while True:
        yield from asyncio.sleep(1)
        print('Hello World Coroutine')

@asyncio.coroutine
def good_evening_coroutine():
    while True:
        yield from asyncio.sleep(1)
        print('Good Evening Coroutine')

print('step: asyncio.get_event_loop()')
loop = asyncio.get_event_loop()

print('step: loop.call_soon(hello_world, loop)')
loop.call_soon(hello_world, loop)
print('step: loop.call_soon(good_evening, loop)')
loop.call_soon(good_evening, loop)
print('step: asyncio.async(hello_world_coroutine)')
asyncio.async(hello_world_coroutine())
print('step: asyncio.async(good_evening_coroutine)')
asyncio.async(good_evening_coroutine())

try:
    loop.run_forever()
except KeyboardInterrupt:
    pass
finally:
    print('step: loop.close()')
    loop.close()

如您所见,每个协程函数都有一个包围着的while循环。我该如何使它像普通的函数一样?即当完成后,在给定的延迟时间后调用自身,而不仅是在那里设置一个循环。


1
为什么你不喜欢循环?带有循环的代码非常明显和易于阅读。 - Andrew Svetlov
4个回答

21

如果你真的想从协程中消除while循环(我不确定为什么你觉得这是必要的;这是实现你所需功能最自然的方式),你可以使用asyncio.async(在Python 3.4.4+上也可以使用asyncio.ensure_future)来安排协程在下一个事件循环迭代中再次运行:

import asyncio

@asyncio.coroutine
def hello_world():
    yield from asyncio.sleep(1)
    print('Hello World')
    asyncio.async(hello_world())

@asyncio.coroutine
def good_evening():
    yield from asyncio.sleep(1)
    print('Good Evening')
    asyncio.async(good_evening())

print('step: asyncio.get_event_loop()')
loop = asyncio.get_event_loop()
try:
    print('step: loop.run_until_complete()')
    asyncio.async(hello_world())
    asyncio.async(good_evening())
    loop.run_forever()
except KeyboardInterrupt:
    pass
finally:
    print('step: loop.close()')
    loop.close()

请注意,如果您这样做,您必须切换回使用loop.run_forever(),因为hello_world/good_evening将立即在打印后退出。


这就是我想要的。谢谢! - Johann Chang

3
# asyncio_coroutine_forever.py

import asyncio

async def hello_world():

    await asyncio.sleep(1)
    print('Hello World')
    await good_evening()


async def good_evening():

    await asyncio.sleep(1)
    print('Good Evening')
    await hello_world()


loop = asyncio.get_event_loop()

try:

    loop.run_until_complete(hello_world())
    loop.run_until_complete(good_evening())
    loop.run_forever()

finally:

    print('closing event loop')
    loop.close()

看起来好多了。谢谢Morten。使用Python 3.6.1测试了该代码。 - Down the Stream
1
虽然由于等待而最终会耗尽堆栈帧,但将协程作为 futures 添加到事件循环中并await asyncio.sleep(1.0)会更好。 - Goodies

1
import asyncio


@asyncio.coroutine
def hello_world_coroutine():
    yield from asyncio.sleep(1)
    print('Hello World Coroutine')
    yield from hello_world_coroutine()

@asyncio.coroutine
def good_evening_coroutine():
    yield from asyncio.sleep(1)
    print('Good Evening Coroutine')
    yield from good_evening_coroutine()

print('step: asyncio.get_event_loop()')
loop = asyncio.get_event_loop()
try:
    print('step: loop.run_until_complete()')
    loop.run_until_complete(asyncio.wait([
        hello_world_coroutine(),
        good_evening_coroutine()
    ]))
except KeyboardInterrupt:
    pass
finally:
    print('step: loop.close()')
    loop.close()

更新

这段代码会达到最大递归深度。可能是因为 Python 没有尾调用优化。将这段代码保留作为错误示例。


我不会立即将其标记为答案。等待一些时间以获得更好的解决方案。 - Johann Chang
1
这实际上不会起作用 - 最终你会达到最大递归深度,程序将崩溃。 - dano

0

你有没有真正尝试运行你提供的三个例子?它们的行为差异非常明显。

由于你从未说明你的期望,因此无法确定哪个是正确的,哪个是错误的。这三种实现都可能是对的或错的。我可以告诉你每个实现的行为以及为什么会有这样的行为;只有你知道它是否正确。


在第二个例子中(yield from asyncio.sleep(1)),两个协程同时运行。这意味着每个协程都会独立执行;hello_world每秒打印一次Hello World,而good_evening每秒打印一次Good Evening
另外两个例子都使用了阻塞的time.sleep(1)。这意味着当第一个函数(无论是哪个;假设是hello_world)到达time.sleep(1)时,整个程序将会停顿一秒钟。这意味着当hello_world休眠时,good_evening也无法运行,反之亦然。
程序的执行方式如下:
  1. 进入循环。
  2. 循环调用hello_world
  3. hello_world中到达time.sleep(1)。程序休眠一秒钟。
  4. 打印Hello World
  5. hello_world产生。
  6. 循环调用good_evening
  7. 打印Good Evening
  8. good_evening中到达time.sleep(1)。程序休眠一秒钟。
  9. good_evening产生。
  10. 转到2。

因此,每隔秒钟就会出现Hello WorldGood Evening,因为每个print之间有两个time.sleep(1)调用。如果您运行这两个示例,您将很容易注意到这一点。


谢谢@uranusjr。我编写了代码并运行了它。我知道它的流程,也知道sleep会阻塞线程并从async中yield出来。但我不确定的是这种方式是否正确或符合Pythonic方式。我知道协程的概念,但以前没有使用过。到目前为止,我没有看到其他人为此目的编写相同的代码。他们使用普通函数和协程中的一个,而不是两者兼备。 - Johann Chang
顶层协程并不能构成异步程序;只有在使用像 asyncio.sleep 这样的异步 API 时,程序才是异步的。你可以在协程中使用阻塞式 API(例如 time.sleep),但阻塞式 API 会阻塞整个程序,包括其他协程。是否可接受取决于你的使用情况。 - uranusjr
再次感谢。我已经更新了问题和代码。请用call_later替换call_soon并删除time.sleep()。 - Johann Chang
更新后的示例对我来说很有意义。现在示例1和2应该表现出相同的行为,而示例3也是真正的异步。 - uranusjr
谢谢@uranusjr。但是我想知道的是如何发出while循环。 - Johann Chang
好的,我明白了。只要从函数本身yield即可。 - Johann Chang

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接