web-dev-qa-db-ja.com

asyncioで関数を定期的に実行するにはどうすればよいですか?

tornadoからasyncioに移行していますが、asynciotornadoに相当するPeriodicCallbackが見つかりません。 (PeriodicCallbackは、実行する関数と呼び出し間のミリ秒数の2つの引数を取ります。)

  • asyncioにそのような同等物はありますか?
  • そうでない場合、しばらくしてRecursionErrorを取得するリスクを冒さずにこれを実装する最もクリーンな方法は何ですか?
45
2Cubed

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
35

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 __== '__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で、そのタスクをキャンセルします。プログラムが終了した時点でタスクを停止する必要があることに注意してください。

もう1つ重要なことは、コールバックの実行にそれほど時間がかからないことです(または、イベントループがフリーズします)。長時間実行されるfuncを呼び出す予定がある場合は、おそらく executorで実行するために が必要になります。

19

定期的な呼び出しに対する組み込みのサポートはありません。

スケジュールされたタスクをスリープして実行する独自のスケジューラループを作成するだけです。

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秒以上かかる場合があります。ここでのアイデアは、スケジューラが最後のチェック以降に開始する必要があるすべてのタスクを生成することです。

15
Martijn Pieters

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 __== '__main__':
    asyncio.run(do_something('Maluzinha do papai!', secret=42))

役立つ可能性のあるバリアント:最後の実行の終了から次の実行の開始までの間に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())
1
Fred Ross

@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
1
Wojciech Migda