如何定期使用asyncio执行功能?


69

我从迁移tornadoasyncio了,我找不到asyncio等效tornadoPeriodicCallback。(APeriodicCallback有两个参数:要运行的函数和两次调用之间的毫秒数。)

  • 有这样的等同物asyncio吗?
  • 如果没有,那么在不冒RecursionError一会儿风险的情况下,最干净的方法是什么呢?


为什么需要从龙卷风中移走?他们可以一起工作,不是吗? tornadoweb.org/en/stable/asyncio.html
OneCricketeer '16

只需添加await asyncio.sleep(time)到您的功能。
songololo '16

与Twisted相同,没有LoopingCall实现。
zgoda

Answers:


60

对于3.5以下的Python版本:

import asyncio

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

def stop():
    task.cancel()

loop = asyncio.get_event_loop()
loop.call_later(5, stop)
task = loop.create_task(periodic())

try:
    loop.run_until_complete(task)
except asyncio.CancelledError:
    pass

对于Python 3.5及更高版本:

import asyncio

async def periodic():
    while True:
        print('periodic')
        await asyncio.sleep(1)

def stop():
    task.cancel()

loop = asyncio.get_event_loop()
loop.call_later(5, stop)
task = loop.create_task(periodic())

try:
    loop.run_until_complete(task)
except asyncio.CancelledError:
    pass

5
即使在Tornado中,PeriodicCallback对于使用协程的应用程序,我也建议使用这样的循环,而不是一个循环。
本·达内尔

8
简要说明一下:不要直接创建Task实例。使用ensure_future()功能或AbstractEventLoop.create_task()方法。来自asyncio文档
TorkelBjørnson-Langen17年

可以使用lambda代替stop函数。即:loop.call_later(5, lambda: task.cancel())
TorkelBjørnson-Langen17年

22
或者,您可以像这样称呼它loop.call_later(5, task.cancel)
重写了

只是Python 3.7的注意事项:从asyncio文档中,我们应该使用高级asyncio.create_task()创建Task
mhchia

29

当您认为应该在asyncio程序的“后台”发生某些事情时,这asyncio.Task也许是个好方法。您可以阅读这篇文章,以了解如何使用任务。

这是定期执行某些功能的类的可能实现:

import asyncio
from contextlib import suppress


class Periodic:
    def __init__(self, func, time):
        self.func = func
        self.time = time
        self.is_started = False
        self._task = None

    async def start(self):
        if not self.is_started:
            self.is_started = True
            # Start task to call func periodically:
            self._task = asyncio.ensure_future(self._run())

    async def stop(self):
        if self.is_started:
            self.is_started = False
            # Stop task and await it stopped:
            self._task.cancel()
            with suppress(asyncio.CancelledError):
                await self._task

    async def _run(self):
        while True:
            await asyncio.sleep(self.time)
            self.func()

让我们测试一下:

async def main():
    p = Periodic(lambda: print('test'), 1)
    try:
        print('Start')
        await p.start()
        await asyncio.sleep(3.1)

        print('Stop')
        await p.stop()
        await asyncio.sleep(3.1)

        print('Start')
        await p.start()
        await asyncio.sleep(3.1)
    finally:
        await p.stop()  # we should stop task finally


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

输出:

Start
test
test
test

Stop

Start
test
test
test

[Finished in 9.5s]

如您所见,start我们只是启动任务,该任务调用一些函数并在无尽循环中休眠一些时间。关于,stop我们只是取消该任务。注意,该程序应在程序完成时停止。

更重要的一件事是您的回调不应该花费太多时间来执行(否则它将冻结事件循环)。如果您打算调用一些长时间运行的程序func,则可能需要在executor中运行它


迄今为止最完整,最清晰的答案!谢谢。要求将func其作为协程是一个好主意,因此我们可以:await self.func()在该_run方法中?
谢尔盖·比什

1
@SergeyBelash,可以,没关系。请注意,由于我们在随机时间取消任务,因此您的功能可能也会在随机时间取消。这意味着函数内的每个等待行都可能引发CancelledError。但这对于所有异步功能都是实际的(就像KeyboardInterrupt可以在常规非异步代码中随机引发一样)。
米哈伊尔·杰拉西莫夫

我对此(以及其他答案)感到担心,重复率将不完全是时间值。如果func需要相当长的时间来执行,它甚至都不会关闭,并且即使func花费的时间可以忽略不计,很长一段时间它也会漂移。
伊恩·戈德比

严格来说,start()并不需要async
fgiraldeau

可以升级为支持普通和异步功能:```async def _run(self):而True:等待asyncio.sleep(self.time)#支持普通和异步功能res = self.func()(如果检查)。 isawaitable(res):等待
res```– Airstriker

23

没有对定期呼叫的内置支持。

只需创建您自己的调度程序循环即可休眠并执行任何调度的任务:

import math, time

async def scheduler():
    while True:
        # sleep until the next whole second
        now = time.time()
        await asyncio.sleep(math.ceil(now) - now)

        # execute any scheduled tasks
        await for task in scheduled_tasks(time.time()):
            await task()

scheduled_tasks()迭代器应该产生对于准备在给定的时间运行的任务。请注意,从理论上讲,制定时间表并启动所有任务可能要花费超过1秒的时间;这里的想法是调度程序产生自上次检查以来应该已经启动的所有任务。


asyncio事件循环有time()可能替代的使用方法time模块。
krs013 '17

3
@ krs013:那是一个不同的时钟;它不一定会给您真实的时间(它取决于事件循环的实现,并且可以测量CPU时间滴答声或其他单调增加的时钟量度)。由于不能保证以秒为单位提供度量,因此此处应该使用它。
马丁·彼得斯

哦,很好,谢谢。我认为对于间隔定时而言这已经足够了,但是似乎无法保证睡眠线程的准确性。我所看到的实现似乎只是在几秒钟内使用机器正常运行时间,但是是的,您是对的。我想我现在有一些代码要解决...
krs013

文档字符串的的loop.time方法的状态“这是因为一个划时代以秒表示浮子,但该历元,精度,准确度和漂移是不确定的,并且可能根据事件循环不同。” 在这里,我将其解释为“自纪元以来的SI秒”,因此CPU时间滴答作响,或者其他非“统一”时钟不符合loop.time()。由于OP只是要求每x毫秒进行一次定期回调,因此在我看来这loop.time()已经足够了。
Stefano M,

@StefanoM:是的,这可能足够了,但是它依赖于事件循环实现,并且文档字符串为实现提供了很大的回旋余地。对于重复任务可能已经足够好了,但是我的答案描述了一个调度程序,它经常需要做类似cron的事情(例如,在特定的实际时间运行任务)。
马丁·彼得斯

12

一种变体可能会有所帮助:如果您希望您的重复调用每n秒而不是上一次执行的结束与下一次执行的开始之间的n秒发生一次,并且您不希望调用在时间上重叠,请执行以下操作:更简单:

async def repeat(interval, func, *args, **kwargs):
    """Run func every interval seconds.

    If func has not finished before *interval*, will run again
    immediately when the previous iteration finished.

    *args and **kwargs are passed as the arguments to func.
    """
    while True:
        await asyncio.gather(
            func(*args, **kwargs),
            asyncio.sleep(interval),
        )

以及使用它在后台运行几个任务的示例:

async def f():
    await asyncio.sleep(1)
    print('Hello')


async def g():
    await asyncio.sleep(0.5)
    print('Goodbye')


async def main():
    t1 = asyncio.ensure_future(repeat(3, f))
    t2 = asyncio.ensure_future(repeat(2, g))
    await t1
    await t2

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

谢谢!我的服务器负载很重时,我遇到了这个问题,在很多次重复中,我们开始出现时钟偏斜的情况。这样可以优雅地解决它。
克里斯蒂安·奥达德

为什么在main()中使用sure_future?为什么不简单地await repeat(3, f)await repeat(2, g)
marcoc88 '20

8

带有python 3.7装饰器的替代版本

import asyncio
import time


def periodic(period):
    def scheduler(fcn):

        async def wrapper(*args, **kwargs):

            while True:
                asyncio.create_task(fcn(*args, **kwargs))
                await asyncio.sleep(period)

        return wrapper

    return scheduler


@periodic(2)
async def do_something(*args, **kwargs):
    await asyncio.sleep(5)  # Do some heavy calculation
    print(time.time())


if __name__ == '__main__':
    asyncio.run(do_something('Maluzinha do papai!', secret=42))

4

基于@A。Jesse Jiryu Davis的回复(带有@TorkelBjørnson-Langen和@ReWrite注释),这是一种避免漂移的改进。

import time
import asyncio

@asyncio.coroutine
def periodic(period):
    def g_tick():
        t = time.time()
        count = 0
        while True:
            count += 1
            yield max(t + count * period - time.time(), 0)
    g = g_tick()

    while True:
        print('periodic', time.time())
        yield from asyncio.sleep(next(g))

loop = asyncio.get_event_loop()
task = loop.create_task(periodic(1))
loop.call_later(5, task.cancel)

try:
    loop.run_until_complete(task)
except asyncio.CancelledError:
    pass

2
periodic应该loop.time()优先使用,time.time()因为loop.time()是内部使用了时间参考asyncio.sleep()loop.time()返回单调时间,而time.time()返回挂钟时间。两者将有所不同,例如,当系统管理员修改系统上的日期或NTP调整挂钟时间时。
user4815162342'2
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.