#Issue with asyncio and events

1 messages · Page 1 of 1 (latest)

cloud moth
#

Hi all,

I am just trying out CircuitPython (8.2.4) on a Rasberry PI Pico W, and am encountering an issue with asyncio and events. Below is my minimal code example:

import asyncio

async def main():
    enable_event = asyncio.Event()
    print("waiting...")
    await enable_event.wait()
    print("done!")

asyncio.run(main())

I would expect this to wait forever, but instead the code exits without ever printing "done!". Here is the output from the REPL:

code.py output:
waiting...

Code done running.

Can anyone help me understand what I am doing wrong (it works as expected in regular Python)? I have tried Google, but could not find anything useful. Thanks in advance!

amber hollow
#

That behavior is a consequence of how asyncio is implemented in CircuitPython. (Bear with me for a long explanation. TL/DR: You didn't do anything wrong, other than code a task deadlock, which this implementation could detect.)

Every task other than the currently active one is on a task queue somewhere. If it's awaiting another task, it's on that task's wait queue. If awaiting an event, on that event's wait queue. If awaiting an asyncio.sleep, it's on the main task queue. Otherwise it's ready to run, also on the main task queue.

#

When an event fires, or when a task ends, all the tasks in the event or task wait queue are moved to the main queue and are ready to run.

#

When the active task awaits something, asyncio's run loop regains control. It puts the task on the appropriate wait queue, then looks at the task at head of the main queue. If it's ready to run, it becomes the active task -- it's popped off the queue and (re)entered. If it's sleeping -- the sleeping tasks are entered into the main queue in order of wakeup time -- then the run loop (busy) waits until it's time to wake it up.

#

If the main queue is empty, then there's nothing more to do, so the run loop returns. Nothing more can happen: a task needs to be running to set an event, or to exit so the tasks awaiting it can be scheduled into the main loop.

#

So that's what happened in your case: your task got put in the event's wait queue, the await yielded to the run loop, and the run loop found the main queue empty, so it returned.

#

(There's one more task queue I didn't talk about, an _io_queue which evidently is tasks waiting for an i/o operation to complete. This queue is examined if the main queue is empty. I'm not sure how it's used in CircPy, The point is that the only things that can happen outside the task code are timer or i/o occurrences.)

cloud moth
#

Thank you for the long explanation!

#

My example was just the minimal I could come up with to show how CircuitPython behaves differently from "regular" Python. In practice I am using Event as a way to communicate between async functions. Function A will await the Event as part of a loop, while function B can set/clear the event and thus pause/resume function A, if that makes sense.

#

This worked fine when I did it in regular python, and it also works if I make my own trivial Event class.