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

on_press виконується в контексті переривання щоразу, коли GPIO спрацьовує; все, що він робить, це викликає flag.set(). Завдання asyncio watcher виконує awaits flag.wait() у циклі; щоразу, коли прапор встановлюється, завдання відновлюється, виконує необхідну обробку натискання кнопки, а потім повертається до очікування.

8.9.3. Три методи

  • set() – позначити прапор як встановлений. Якщо завдання зараз очікує в wait(), запланувати його відновлення. Безпечно викликати з обробника переривань.

  • wait() – корутина. Блокується до встановлення прапора, потім повертається. Прапор автоматично скидається після повернення – наступний виклик wait буде заблокований знову до наступного set().

  • clear() – явно скинути прапор без очікування на нього. Корисно перед першим wait, щоб відкинути будь-які хибні ранні set(), що спрацювали до готовності завдання.

8.9.4. Лише один очікувач

Лише одне завдання може перебувати в wait() одночасно. Правильний шаблон – мати одне завдання-власника для прапора, зазвичай те саме завдання, що встановило обробник переривань, а інші завдання спілкуються з ним через Event, Lock або спільні структури даних, захищені цими примітивами.

Коли кнопка має пробуджувати кілька завдань, завдання-власник є місцем розгалуження: наведений вище спостерігач міг би викликати some_event.set() після кожного натискання, і всі завдання, що чекають на some_event, відновились би.