Использование asyncio для запуска функции в начале (00 секунд) каждой минуты

Я пытаюсь запустить несколько функций одновременно (приблизительно или курс) с различными параметрами и повторять это в начале каждой минуты.

Мне удалось получить asyncio Пример запуска, где я получаю функцию callback запускать в определенное время с другим параметром, но я не могу понять, как запустить его (и продолжать его вечно) в очень конкретные моменты времени (т.е. я хочу запускать его в начале каждой минуты, поэтому в 19:00:00, 19:01:00 и т. Д.).

Asyncio call_at должен быть в состоянии сделать это, но он использует формат времени, который не является стандартным форматом времени Python, и я не могу определить, чтобы указать этот формат времени в качестве 00 секунд следующей минуты.

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()

3 ответа

Решение

Как отмечали некоторые комментаторы, не существует простого и гибкого способа сделать это на чистом Python только с asyncIO, но с библиотекой apscheduler это становится довольно просто.

from apscheduler.schedulers.asyncio import AsyncIOScheduler
import asyncio
import os

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

Как уже отмечали другие, для такого рода вещей нет встроенной функциональности, вам нужно будет написать это самостоятельно. Это просто реализовать, хотя - простая версия может выглядеть так:

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()

Это не использовать call_laterэто сопрограмма, которая может быть отменена, когда в ней больше нет необходимости. Это просто берет значение секунд текущего времени на стене xи спит (60 - x) чтобы добраться до следующей минуты. Вот тест:

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
...

К сожалению, простая реализация может плохо себя вести, если asyncio.sleep случается, что он спит чуть-чуть короче, чем запрашиваемый период, например, из-за перекоса часов. В этом случае последующее asyncio.sleep будет пытаться снова достичь начала той же минуты и поспать всего лишь доли секунды, что приведет к быстрому последовательному обратному вызову, который будет выполнен дважды. Чтобы предотвратить это, необходим дополнительный код для компенсации коротких снов:

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

Там нет простого способа сделать это. Вы не можете положиться на loop.time() как "реальные" часы.

Единственный способ обойти это считать время datetime/time модули и вызов loop.call_later с расчетной задержкой. И да, это супер громоздко.

Посмотрите на этот вопрос для примера: Как я могу периодически выполнять функцию с asyncio?

Другие вопросы по тегам