11.9. ThreadSafeFlag

asyncio.ThreadSafeFlag is the signalling primitive asyncio provides for the case Event does not support: an interrupt handler needs to wake an asyncio task up. The interrupt fires outside the event loop’s normal scheduling, so the bookkeeping asyncio.Event.set() does cannot be done safely from inside it.

11.9.1. Why a separate primitive

asyncio.Event.set() runs the bookkeeping that wakes waiting coroutines under the assumption that it is being called from inside a task the loop is currently running. An interrupt handler does not satisfy that assumption – it can fire between any two instructions and corrupt whatever the loop was halfway through doing.

asyncio.ThreadSafeFlag.set() is written to be safe in those contexts. The trade-off for that safety is a smaller feature set than Event: a ThreadSafeFlag can only be waited on by one task at a time, and the flag auto-resets when the waiting task wakes up.

11.9.2. The basic shape

A typical use is a GPIO interrupt handing off a button-press event to an asyncio task:

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 runs in interrupt context whenever the GPIO fires; all it does is call flag.set(). The asyncio task watcher awaits flag.wait() in a loop; each time the flag is set, the task resumes, runs whatever work the button press requires, then loops back to wait again.

11.9.3. The three methods

  • set() – mark the flag as set. If a task is currently waiting in wait(), schedule it to resume. Safe to call from an interrupt handler.

  • wait() – a coroutine. Block until the flag is set, then return. The flag is automatically cleared on return – the next call to wait will block again until the next set().

  • clear() – explicitly clear the flag without waiting on it. Useful before a first wait to discard any spurious early set() that fired before the task was ready.

11.9.4. One waiter only

Only one task may be in wait() at a time. The shape an application wants is a single owner task for the flag – usually the same task that installed the interrupt handler – with other tasks communicating with it through Event, Lock, or shared data structures protected by those primitives.

When a button has to wake several tasks, the owner task is the place to fan out: the watcher above could call some_event.set() after each press, and all the tasks waiting on some_event would resume.