8.9. ThreadSafeFlag

asyncio.ThreadSafeFlag は、Eventサポートしない ケース、すなわち 割り込みハンドラが asyncio タスクを起こす必要がある 場合のために asyncio が提供するシグナリングプリミティブです。割り込みはイベントループの通常のスケジューリングの外側で発生するため、asyncio.Event.set() が行う管理処理を割り込みの内側から安全に行うことはできません。

8.9.1. なぜ別個のプリミティブが必要か

asyncio.Event.set() は、待機中のコルーチンを起こす管理処理を実行しますが、これは ループが現在実行しているタスクの内側から呼び出されている という前提のもとに行われます。割り込みハンドラはこの前提を満たしません。割り込みは任意の 2 つの命令の間で発生する可能性があり、ループが途中まで行っていた処理を破壊してしまうことがあります。

asyncio.ThreadSafeFlag.set() は、そうしたコンテキストでも安全になるように書かれています。その安全性と引き換えに、機能は Event よりも少なくなります。ThreadSafeFlag は一度に 1 つ のタスクからしか待機できず、待機中のタスクが起きるとフラグは自動的にリセットされます。

8.9.2. 基本的な形

典型的な用途は、ボタン押下イベントを GPIO 割り込みから asyncio タスクへ受け渡すケースです:

import asyncio
from machine import Pin

flag = asyncio.ThreadSafeFlag()

def on_press(pin):
    flag.set()

async def watcher():
    button = Pin("P1", Pin.IN, Pin.PULL_UP)
    button.irq(handler=on_press, trigger=Pin.IRQ_FALLING)
    while True:
        await flag.wait()
        print("button pressed")

asyncio.run(watcher())

on_press は GPIO が発火するたびに割り込みコンテキストで実行され、その役割は flag.set() を呼び出すことだけです。asyncio タスク watcher はループ内で flag.wait()await します。フラグがセットされるたびにタスクが再開し、ボタン押下が要求する処理を実行し、その後ループに戻って再び待機します。

8.9.3. 3 つのメソッド

  • set() -- フラグをセット済みとしてマークします。wait() で現在待機しているタスクがあれば、それが再開するようにスケジュールします。割り込みハンドラから呼び出しても安全です。

  • wait() -- コルーチンです。フラグがセットされるまでブロックし、その後返ります。返るときにフラグは 自動的にクリア されます。次の wait の呼び出しは、次の set() まで再びブロックします。

  • clear() -- 待機せずにフラグを明示的にクリアします。最初の wait の前に、タスクが準備できる前に発火した不要な早すぎる set() を破棄するのに便利です。

8.9.4. 待機者は 1 つだけ

一度に wait() に入れるタスクは 1 つだけです。アプリケーションが望む形は、フラグに対して単一の 所有者 タスク(通常は割り込みハンドラを設置したのと同じタスク)を持ち、他のタスクは EventLock、あるいはそれらのプリミティブで保護された共有データ構造を通じてそれと通信する、というものです。

ボタンが複数のタスクを起こさなければならない場合、所有者タスクが分配を行う場所になります。上の watcher は各押下のあとに some_event.set() を呼び出すことができ、some_event で待機しているすべてのタスクが再開します。