简体   繁体   English

Python - 带有异步/协程的定时器

[英]Python - Timer with asyncio/coroutine

I am trying to set a timer that will interrupt the running process and call a coroutine when it fires.我正在尝试设置一个计时器,该计时器将中断正在运行的进程并在它触发时调用协程。 However, I'm not sure what the right way to accomplish this is.但是,我不确定实现这一目标的正确方法是什么。 I've found AbstractEventLoop.call_later, along with threading.Timer but neither of these seem to work (or I'm using them incorrectly).我找到了 AbstractEventLoop.call_later 和 threading.Timer 但这些似乎都不起作用(或者我没有正确使用它们)。 The code is pretty basic and looks something like this:代码非常基本,看起来像这样:

def set_timer( time ):
    self.timer = Timer( 10.0, timeout )
    self.timer.start()
    #v2
    #self.timer = get_event_loop()
    #self.timer.call_later( 10.0, timeout )
    return

async def timeout():
    await some_func()
    return

What is the correct way to set a non-blocking timer, that will call a callback function after some number of seconds?设置非阻塞计时器的正确方法是什么,它将在几秒钟后调用回调函数? Being able to cancel the timer would be a bonus but is not a requirement.能够取消计时器将是一种奖励,但不是必需的。 The major things I need are: non-blocking and successfully calling the co-routine.我需要的主要东西是:非阻塞和成功调用协程。 Right now it returns an error that the object can't be await'd (if I toss an await in) or that some_func was never await'd, and the expected output never happens.现在它返回一个错误,该对象不能被等待(如果我抛出一个等待)或者 some_func 从未被等待,并且预期的输出永远不会发生。

Creating Task using ensure_future is a common way to start some job executing without blocking your execution flow.使用ensure_future创建任务是在不阻塞执行流程的情况下开始执行某些作业的常用方法。 You can also cancel tasks.您也可以取消任务。

I wrote example implementation for you to have something to start from:我为您编写了示例实现,以便从以下方面着手:

import asyncio


class Timer:
    def __init__(self, timeout, callback):
        self._timeout = timeout
        self._callback = callback
        self._task = asyncio.ensure_future(self._job())

    async def _job(self):
        await asyncio.sleep(self._timeout)
        await self._callback()

    def cancel(self):
        self._task.cancel()


async def timeout_callback():
    await asyncio.sleep(0.1)
    print('echo!')


async def main():
    print('\nfirst example:')
    timer = Timer(2, timeout_callback)  # set timer for two seconds
    await asyncio.sleep(2.5)  # wait to see timer works

    print('\nsecond example:')
    timer = Timer(2, timeout_callback)  # set timer for two seconds
    await asyncio.sleep(1)
    timer.cancel()  # cancel it
    await asyncio.sleep(1.5)  # and wait to see it won't call callback


loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
    loop.run_until_complete(main())
finally:
    loop.run_until_complete(loop.shutdown_asyncgens())
    loop.close()

Output:输出:

first example:
echo!

second example:

Thanks Mikhail Gerasimov for your answer, it was very useful.感谢 Mikhail Gerasimov 的回答,这非常有用。 Here is an extension to Mikhail's anwer.这是对米哈伊尔的回答的扩展。 This is an interval timer with some twists.这是一个有一些曲折的间隔计时器。 Perhaps it is useful for some users.也许它对某些用户有用。

import asyncio


class Timer:
    def __init__(self, interval, first_immediately, timer_name, context, callback):
        self._interval = interval
        self._first_immediately = first_immediately
        self._name = timer_name
        self._context = context
        self._callback = callback
        self._is_first_call = True
        self._ok = True
        self._task = asyncio.ensure_future(self._job())
        print(timer_name + " init done")

    async def _job(self):
        try:
            while self._ok:
                if not self._is_first_call or not self._first_immediately:
                    await asyncio.sleep(self._interval)
                await self._callback(self._name, self._context, self)
                self._is_first_call = False
        except Exception as ex:
            print(ex)

    def cancel(self):
        self._ok = False
        self._task.cancel()


async def some_callback(timer_name, context, timer):
    context['count'] += 1
    print('callback: ' + timer_name + ", count: " + str(context['count']))

    if timer_name == 'Timer 2' and context['count'] == 3:
        timer.cancel()
        print(timer_name + ": goodbye and thanks for all the fish")


timer1 = Timer(interval=1, first_immediately=True, timer_name="Timer 1", context={'count': 0}, callback=some_callback)
timer2 = Timer(interval=5, first_immediately=False, timer_name="Timer 2", context={'count': 0}, callback=some_callback)

try:
    loop = asyncio.get_event_loop()
    loop.run_forever()
except KeyboardInterrupt:
    timer1.cancel()
    timer2.cancel()
    print("clean up done")

The solution proposed by Mikhail has one drawback. Mikhail 提出的解决方案有一个缺点。 Calling cancel() cancels both: the timer and the actual callback (if cancel() fired after timeout is passed, but actual job is still in progress).调用cancel()取消:计时器和实际回调(如果在超时后触发cancel() ,但实际作业仍在进行中)。 Canceling the job itself may be not the desired behavior.取消作业本身可能不是理想的行为。

An alternative approach is to use loop.call_later :另一种方法是使用loop.call_later

async def some_job():
    print('Job started')
    await asyncio.sleep(5)
    print('Job is done')

loop = asyncio.get_event_loop() # or asyncio.get_running_loop()

timeout = 5
timer = loop.call_later(timeout, lambda: asyncio.ensure_future(some_job()))

timer.cancel() # cancels the timer, but not the job, if it's already started

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM