extmod/uasyncio Event.set() not safe in ISR
If you call Event.set() in an ISR, you could end up losing all tasks that were awaiting the event (if the ISR is executed while uasyncio is sorting tasks on the queue e.g.).
I made a testscript a while ago to demonstrate this: https://gist.github.com/kevinkk525/5b89395604da3df3be7a015abcba04fa
uasyncio: make uasyncio.Event() safe to call from an interrupt (RFC, WIP)
The set of commits in this PR add an interface between soft interrupt handlers (ie micropython.schedule calls) and the uasyncio module. For example, one can use it to build an async-pin which can be awaited on for an edge or level change.
There's a lot to discuss here, and at the moment it's more of a proof-of-concept to show what's necessary to make it work.
The approach taken here has two parts to it:
- add a way for the asyncio scheduler (namely
select.poll) to be woken asynchronously/externally - make
uasyncio.Event()"thread safe" so it can be set from a soft interrupt handler, ie from something scheduled bymicropython.schedule
The solution for the first part was chosen so that it will work on the unix port, and it's also the same way that CPython works: create a special socket.socketpair() (like a unix file pipe), register that with select.poll and then write to it to signal that the asyncio scheduler should wake up. This is thread-safe and race-free (the signal can come in just before poll is called and it'll still work).
The second part (making Event.set() thread safe) is done for efficiency: even if there are hundreds of external events (eg pin irqs, UARTs, ble events, etc) they do not put a burden on poll because only the socketpair (and IO streams) are registered with poll.
The new things in this PR are:
- addition of
micropython.schedule_lock()andmicropython.schedule_unlock() - addition of
usocket.socketpair()including a bare-metal version for ports that need it - improve
MICROPY_EVENT_POLL_HOOKon stm32 to be atomic wrt waiting for events - make
uasyncio.Event()thread/IRQ safe
The way to use this feature is:
- create an
uasyncio.Event()corresponding to the external event - tasks wait on this event
- IRQ triggers soft callback via
micropython.schedule() - that callback will set the
Event-- internally the event will schedule all waiting tasks (immediately in the soft callback) and then notify the asyncio poll via the socketpair that it should wake up and run the next task