8.9. ThreadSafeFlag

asyncio.ThreadSafeFlag ist die Signalisierungsprimitive, die asyncio für den Fall bereitstellt, den Event nicht unterstützt: ein Interrupt-Handler muss einen asyncio-Task aufwecken. Der Interrupt feuert außerhalb der normalen Planung des Event-Loops, sodass die Buchführung, die asyncio.Event.set() durchführt, nicht sicher aus dem Inneren des Handlers heraus erfolgen kann.

8.9.1. Warum eine separate Primitive

asyncio.Event.set() führt die Buchführung, die wartende Coroutinen aufweckt, unter der Annahme aus, dass es aus dem Inneren eines Tasks aufgerufen wird, den der Loop gerade ausführt. Ein Interrupt-Handler erfüllt diese Annahme nicht – er kann zwischen zwei beliebigen Anweisungen feuern und beschädigen, was der Loop gerade halb erledigt hatte.

asyncio.ThreadSafeFlag.set() ist so geschrieben, dass es in diesen Kontexten sicher ist. Der Preis für diese Sicherheit ist ein kleinerer Funktionsumfang als bei Event: Auf eine ThreadSafeFlag kann immer nur ein Task gleichzeitig warten, und das Flag setzt sich automatisch zurück, wenn der wartende Task aufwacht.

8.9.2. Die grundlegende Struktur

Eine typische Verwendung ist ein GPIO-Interrupt, der ein Tastendruck-Ereignis an einen asyncio-Task übergibt:

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 läuft im Interrupt-Kontext, wann immer der GPIO feuert; alles, was es tut, ist flag.set() aufzurufen. Der asyncio-Task watcher awaitet flag.wait() in einer Schleife; jedes Mal, wenn das Flag gesetzt wird, nimmt der Task die Ausführung wieder auf, erledigt die Arbeit, die der Tastendruck erfordert, und kehrt dann zur Schleife zurück, um erneut zu warten.

8.9.3. Die drei Methoden

  • set() – markiert das Flag als gesetzt. Wenn ein Task gerade in wait() wartet, wird seine Fortsetzung eingeplant. Sicher aus einem Interrupt-Handler aufrufbar.

  • wait() – eine Coroutine. Blockiert, bis das Flag gesetzt ist, und kehrt dann zurück. Das Flag wird beim Zurückkehren automatisch gelöscht – der nächste Aufruf von wait blockiert erneut, bis zum nächsten set().

  • clear() – löscht das Flag explizit, ohne darauf zu warten. Nützlich vor einem ersten wait, um ein verfrühtes, unbeabsichtigtes set() zu verwerfen, das gefeuert hat, bevor der Task bereit war.

8.9.4. Nur ein Wartender

Immer nur ein Task darf gleichzeitig in wait() sein. Die Struktur, die eine Anwendung anstrebt, ist ein einzelner Eigentümer-Task für das Flag – üblicherweise derselbe Task, der den Interrupt-Handler installiert hat – wobei andere Tasks über Event, Lock oder gemeinsam genutzte Datenstrukturen, die durch diese Primitiven geschützt sind, mit ihm kommunizieren.

Wenn eine Taste mehrere Tasks aufwecken muss, ist der Eigentümer-Task der Ort, um aufzufächern: Der obige Watcher könnte nach jedem Druck some_event.set() aufrufen, und alle Tasks, die auf some_event warten, würden die Ausführung wieder aufnehmen.