8.9. ThreadSafeFlag

asyncio.ThreadSafeFlag 是 asyncio 针对 Event 支持的场景所提供的信号原语:中断处理程序需要唤醒一个 asyncio 任务。中断在事件循环的正常调度之外触发,所以 asyncio.Event.set() 所做的簿记工作无法安全地从中断内部完成。

8.9.1. 为何需要单独的原语

asyncio.Event.set() 在执行唤醒等待协程的簿记工作时,假定它是从事件循环当前正在运行的某个任务内部被调用的。中断处理程序不满足这个假定——它可能在任意两条指令之间触发,从而破坏事件循环正在中途进行的任何工作。

asyncio.ThreadSafeFlag.set() 的编写方式使其在那些上下文中是安全的。这种安全性的代价是其功能集比 Event 更小:一个 ThreadSafeFlag 同一时刻只能被一个任务等待,并且在等待任务被唤醒时该标志会自动复位。

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())

每当 GPIO 触发时,on_press 在中断上下文中运行;它所做的全部就是调用 flag.set()。asyncio 任务 watcher 在循环中 await flag.wait();每次标志被设置时,该任务恢复运行,执行按键所需的任何工作,然后再循环回去等待。

8.9.3. 三个方法

  • set() ——将标志标记为已设置。如果有任务当前正在 wait() 中等待,则调度它恢复运行。可以安全地从中断处理程序中调用。

  • wait() ——一个协程。阻塞直到标志被设置,然后返回。标志在返回时会被自动清除——下一次调用 wait 将再次阻塞,直到下一次 set()

  • clear() ——在不等待标志的情况下显式清除它。在首次 wait 之前调用很有用,可以丢弃任何在任务就绪之前触发的虚假早期 set()

8.9.4. 仅限一个等待者

同一时刻只能有一个任务处于 wait() 中。应用程序所需要的写法是为该标志设置单一的所有者任务——通常就是安装中断处理程序的那个任务——而其他任务通过 EventLock,或由这些原语保护的共享数据结构与它通信。

当一次按键必须唤醒多个任务时,所有者任务就是进行扇出的地方:上面的 watcher 可以在每次按键后调用 some_event.set(),于是所有等待 some_event 的任务都会恢复运行。