8.2. コルーチンとタスク

コルーチンは asyncio プログラムを構築する作業単位であり、タスクはアプリケーションが複数のコルーチンを並行して実行する方法です。

8.2.1. コルーチン

コルーチンasync def で宣言された関数です:

import asyncio

async def heartbeat(interval_ms):
    while True:
        print("tick")
        await asyncio.sleep_ms(interval_ms)

本体は通常の関数のように見えますが、1 つの追加要素があります:await です。コルーチンが何か — スリープ、ネットワーク読み込み、設定されるイベント — を待たなければならない場所では、待っているものが準備できるまでコルーチンを中断する方法を知っている式を await します。各 await でコルーチンは asyncio に制御を返します。await された操作が完了すると、asyncio は同じ地点からそれを再開します。

asyncio モジュールは 2 つのスリープを提供します:

  • asyncio.sleep() — 引数は秒単位で、float を受け付けます。

  • asyncio.sleep_ms() — 引数はミリ秒単位で、int を取ります。MicroPython の拡張です。ファームウェアのタイミング調整がミリ秒単位の形をしているため、通常はカメラ上で正しい選択となります。

単なる async def はそれ自体では 何もしませんheartbeat(500) を呼び出しても本体は実行されません。それは asyncio がスケジュールしなければならないコルーチン オブジェクト を返します。1 つをスケジュールする最も簡単な方法は asyncio.run() です:

asyncio.run(heartbeat(500))

asyncio.run() はイベントループを起動し、渡されたコルーチンをトップレベルのエントリポイントとしてスケジュールし、そのコルーチンが返るまでループを駆動し、その後ループを解体します。単一のコルーチンの場合、それがプログラム全体です。複数のコルーチンの場合、アプリケーションはタスクに頼ります。

8.2.2. タスク

タスク は、これを現在のものと並行してスケジュールし、自分は続行させてくれ と告げる、コルーチンに対する asyncio のラッパーです。asyncio.create_task() は 1 つを作成し、スケジュールされた作業を表す Task オブジェクトを 返します:

task = asyncio.create_task(heartbeat("fast", 100))

コルーチンは今やループのスケジュールに乗っています。呼び出し元はそれを待っていません。返された Task は、呼び出し元がその後その実行中の作業とやり取りするために使用するハンドルです。

アプリケーションがハンドルを得たら、それを使って 3 つのことができます:

  • タスクの完了を待つ。 Task はそれ自体が await 可能です。result = await tasktask のコルーチンが返るまで現在のコルーチンを中断し、その後そのコルーチンが返したもの(または送出したものを再送出して)で再開します。

  • タスクをキャンセルする。 task.cancel() は、タスクのコルーチン内の次の awaitasyncio.CancelledError が送出されるようスケジュールし、finally ブロックでクリーンアップコードを実行する機会を与えます。詳細は タイムアウトとキャンセル のページで扱います。

  • 後で識別する。 asyncio.current_task() は現在実行中のコルーチンの Task を返します。ほとんどのスクリプトはこれを呼び出すことはありません。これは計装や例外ハンドラで登場します。

スクリプトは毎回ハンドルをキャプチャする必要はありません。アプリケーションが起動して実行させたままにする使い捨てのバックグラウンドタスクは、戻り値を破棄できます。ループは依然としてそれらをスケジュールします:

import asyncio

async def heartbeat(name, interval_ms):
    while True:
        print(name)
        await asyncio.sleep_ms(interval_ms)

async def main():
    asyncio.create_task(heartbeat("fast", 100))
    asyncio.create_task(heartbeat("slow", 500))
    await asyncio.sleep(5)

asyncio.run(main())

2 つの create_task 呼び出しは、どちらも待つことなく両方の heartbeat をスケジュールします。制御は直ちに main に戻り、続いて 5 秒間のスリープを await します。スリープしている間に 2 つの heartbeat タスクが進行します。ループは実行可能なタスクを順に巡回します。5 秒後に main が返ると、ループはまだ生きているタスクを解体し、asyncio.run() が呼び出し元に返ります。

アプリケーションが上記の 3 つの操作のいずれかを実際に必要とするときは常にハンドルをキャプチャしてください。実際には、それは ほぼ常に を意味します。なぜなら、アプリケーションをきれいにシャットダウンするということは、それが生成したバックグラウンドタスクをキャンセルすることを意味するからです — キャンセルのページがそのパターンを扱います。

8.2.3. 2 行のルール

最小限の asyncio プログラムは、上記の例が終わる 2 行 です:

async def main():
    ...

asyncio.run(main())

それ以外のすべて — アプリケーションが作成するタスク、それらを調整するためのプリミティブ、開くストリーム — は main内部(および main が生成するコルーチンの内部)で起こります。スクリプトがカメラの古典的な while True: csi0.snapshot() ループを超えて成長したとき、答えは asyncio.run() を複数の場所で呼び出すことではありません。新しい作業をより多くのタスクとして main に折り込むことです。