2.32. コルーチン¶
コルーチン とは、途中で一時停止し、後でローカル変数をすべて保持したまま中断したところから再開できる関数です。これはジェネレータのパターン(中断と再開ができる関数本体)を反映していますが、各再開を駆動するのが誰かという点が一つだけ異なります。
コルーチンを記述するためのPythonの構文は、async / await というキーワードのペアです。async は関数をコルーチンとしてマークし、await はその内部で一時停止が許される箇所をマークします。
2.32.1. コルーチンの定義¶
async def で定義された関数は コルーチン関数 です。これを呼び出しても本体は実行されず、まだ開始していない コルーチンオブジェクト が返されます。
async def greet(name):
print("hello,", name)
coro = greet("Alice") # body NOT run yet
コルーチンオブジェクトは関数のまさに先頭で一時停止しています。本体を実行させるには何かがそれを 駆動 する必要があり、それがイベントループというランタイムコンポーネントです。MicroPythonは asyncio モジュールに一つ搭載しています。今のところ、コルーチンは「実行可能な状態で、駆動するものを待っている」とみなしてください。
2.32.2. コルーチン内での一時停止¶
コルーチンの内部では、await 式が、待機している値が準備できるまで実行を中断します。
async def fetch_and_log():
data = await read_sensor()
print("got:", data)
本体が await read_sensor() に到達すると、コルーチンはそれを実行している側に制御を返します。read_sensor() が完了すると、駆動側は結果を data に束縛した状態で次の行からコルーチンを再開します。
await はコルーチンの内部でのみ有効です。通常の関数で使うと構文エラーになります。
2.32.3. ジェネレータとの関係¶
コルーチンとジェネレータは同じ基盤メカニズムを共有しています。違いは各再開を誰が引き出すかです。
ジェネレータは 値 を生成します。消費側は
next()で、あるいは反復処理によって次の値を引き出します。コルーチンは 制御 を譲ります。イベントループが、待機していた操作が準備できたときに再開をスケジュールします。
ジェネレータの yield のハンドシェイクが理解できれば、コルーチンのハンドシェイクも同じ考え方です。違いは for ループの代わりにイベントループが駆動するという点だけです。
イベントループ とは、何か(タイマー、ネットワークイベント、別のコルーチンの完了)を待っているコルーチンのリストを保持する小さなディスパッチャです。各反復で、待機条件が満たされたコルーチンを一つ選び、次の await まで再開し、そのコルーチンが今度は何を待っているかを記録して、別の準備ができたコルーチンへ移ります。その結果、多数のタスクが単一のスレッド上で並行して進行します。各コルーチンは自身の await 地点で自発的に制御を譲り、ループはその合間を、進行可能な他のコルーチンで埋めていきます。
内部では、await と yield は関数を中断・再開するために同じPythonランタイム機能を使っています。キーワードが異なるのは、それらを取り巻く慣習が異なるためです。yield は next() で引き出す消費側に値を返し、await は、待機していた操作が準備できたときに再開をスケジュールするイベントループに制御を渡します。async / await は本質的にコルーチンパターンのより新しい構文です。古いライブラリは、コルーチン間で中断を委譲するために yield from(イテレータとジェネレータ で導入)を用いて、ジェネレータの仕組みの上に直接コルーチンを構築していました。
2.32.4. コルーチンには駆動するものが必要¶
コルーチンは、それを駆動するランタイムがなければ動きません。定義するだけなら問題ありませんが、実行するにはイベントループが必要です。MicroPythonの asyncio モジュールがそのイベントループを提供します。Asyncio のセクションでは、ループの開始方法、そこへのコルーチンのスケジューリング、ロックやイベントによるコルーチン間での状態共有、キャンセルやタイムアウトの処理、そしてここで導入した async / await キーワードを中心に実際のアプリケーションを組み立てる方法を扱います。