简体   繁体   中英

Using asyncio to run a function at the start (00 seconds) of every minute

I'm trying to run several functions at the same time (approximately or course) with different parameters and repeat that at the start of every minute.

I managed to get an asyncio example to run where I get a function callback to run at specific times with a different parameter, but what I can't figure out is how to run it (and keep running it forever) at very specific times (ie I want to run it at the start of every minute, so at 19:00:00, 19:01:00, etc..).

Asyncio call_at should be able to do that, but it uses a time format that is not the standard python time format and I can't figure out to specify that time format as the 00 seconds of the next minute.

import asyncio
import time


def callback(n, loop, msg):
    print(msg)
    print('callback {} invoked at {}'.format(n, loop.time()))


async def main(loop):
    now = loop.time()
    print('clock time: {}'.format(time.time()))
    print('loop  time: {}'.format(now))

    print('registering callbacks')
    loop.call_at(now + 0.2, callback, 1, loop, 'a')
    loop.call_at(now + 0.1, callback, 2, loop, 'b')
    loop.call_soon(callback, 3, loop, 'c')

    await asyncio.sleep(1)


event_loop = asyncio.get_event_loop()
try:
    print('entering event loop')
    event_loop.run_until_complete(main(event_loop))
finally:
    print('closing event loop')
    event_loop.close()

As others have noted, there is no built-in functionality for that kind of thing, you will need to write it yourself. It is straightforward to implement, though - a simple version could look like this:

import asyncio, datetime

async def at_minute_start(cb):
    while True:
        now = datetime.datetime.now()
        after_minute = now.second + now.microsecond / 1_000_000
        if after_minute:
            await asyncio.sleep(60 - after_minute)
        cb()

This doesn't use call_later , it is a coroutine which can be canceled when no longer necessary. It simply takes the seconds value of the current wallclock time x , and sleeps (60 - x) to reach the next minute. Here is a test:

import time
loop = asyncio.get_event_loop()
loop.create_task(at_minute_start(lambda: print(time.asctime())))
loop.run_forever()

# output:
Wed Feb 28 21:36:00 2018
Wed Feb 28 21:37:00 2018
Wed Feb 28 21:38:00 2018
...

Unfortunately, the simple implementation can misbehave if asyncio.sleep ever happens to sleep a tiny bit shorter than the requested period, eg due to clock skew. In that case the subsequent asyncio.sleep will try to again reach the start of the same minute and sleep for only a fraction of a second, resulting in the callback effectively firing twice in quick succession. To prevent that, additional code is needed to compensate for the short sleeps:

async def at_minute_start(cb):
    now = datetime.datetime.now()
    wait_for_next_minute = False
    while True:
        after_minute = now.second + now.microsecond / 1_000_000
        if after_minute != 0:
            to_next_minute = 60 - after_minute
        else:
            to_next_minute = 0  # already at the minute start
        if wait_for_next_minute:
            to_next_minute += 60
        await asyncio.sleep(to_next_minute)
        cb()
        prev = now
        now = datetime.datetime.now()
        # if we're still at the same minute, our sleep was slightly
        # too short, so we'll need to wait an additional minute
        wait_for_next_minute = now.minute == prev.minute

Like some commentators stated, there is no easy a resilient way to do this in pure Python with only asyncIO, but with the apscheduler library it becomes actually quite easy.

import asyncio
import datetime
import os

from apscheduler.schedulers.asyncio import AsyncIOScheduler


def tick():
    print("Tick! The time is: %s" % datetime.datetime.now())


if __name__ == "__main__":
    scheduler = AsyncIOScheduler()
    scheduler.add_job(tick, "cron", minute="*")
    scheduler.start()
    print("Press Ctrl+{0} to exit".format("Break" if os.name == "nt" else "C"))

    # Execution will block here until Ctrl+C (Ctrl+Break on Windows) is pressed.
    try:
        asyncio.get_event_loop().run_forever()
    except (KeyboardInterrupt, SystemExit):
        pass

There's no simple way to do this. You can't rely on loop.time() as "real-world" clock.

Only way around is to count the timedelta with datetime / time modules and call loop.call_later with calculated delay. And yes, its super-cumbersome.

Take a look at this question for examples: How can I periodically execute a function with asyncio?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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