ESP32 In MicroPython: Asyncio
Written by Harry Fairhead & Mike James   
Monday, 09 September 2024
Article Index
ESP32 In MicroPython: Asyncio
Awaiting Sleep
Sequential and Concurrent
Shared Variables and Locks
Using uasyncio

Sequential and Concurrent

The gather function is usually described as being a way of running task coroutines concurrently, but it is better thought of as a way of running and waiting for a set of them to complete:

uasyncio.gather(tasks, return_exceptions = False)

Any coroutine in the task’s comma-separated list of tasks is added to the task queue as a Task. All of the tasks are then executed as the queue schedules them with the calling coroutine suspended until all of them complete. The function returns a list of results in the same order as the awaitables were originally listed in tasks. If return_exceptions is False then any exception is propagated to the calling coroutine, but the other coroutines in the tasks list are left to complete. If it is True then the exceptions are returned as valid results in the list.

If the gather is canceled all of the items in aws are canceled. If any of the items in aws are canceled then it raises a CancelledError, which is either passed to the calling coroutine or added to the result list and the gather itself is not canceled. Using gather is almost the same as adding coroutines to the event loop using asyncio.create_task and then waiting for them to complete. For example:

import uasyncio
async def test1(msg):
    print(msg)
    return msg
async def main():
    result = await uasyncio.gather(
test1("one"),test1("two")) print(result) print("Hello Coroutine World") uasyncio.run(main())

This starts two copies of the test1 coroutine running as tasks on the task loop. As we await them immediately the main coroutine pauses until both are complete and then prints the results as a list. As test1 doesn’t release the main thread the first task runs to completion and then the second is run. This isn’t necessarily the case if either of the tasks releases the thread. The order in which they execute in isn’t determined by the order in which they occur in the gather.

It should be obvious by this point, but is worth making clear, that if you want to run coroutines one after another, i.e. sequentially, then you should use:

await coroutine1()
await coroutine2()
await coroutine3()

and so on. This adds each coroutine to the task loop queue one at a time and the calling coroutine waits for each one to end in turn. If the called coroutine releases the main thread other coroutines, if any, already on the task loop get a chance to run. You can be sure, however, that coroutine1 completes before coroutine2 starts and coroutine2 completes before coroutine3 starts.

Compare this to:

await gather(coroutine1(), coroutine2(), coroutine3(), …)

which adds all of the coroutines to the event loop and then runs each one in turn. If any of the coroutines releases the main thread the other coroutines listed get a chance to run, along with anything that was on the event loop before the gather. As a result all of the coroutines in the gather make progress to completion at the same time, if this is possible. They are executed concurrently in the sense that you cannot be sure that coroutine1 completes before coroutine2 or coroutine3 starts.

esppython360

Canceling Tasks

In a single-threaded environment canceling a Task cannot mean canceling the thread because it has other work to do. If you use the task.cancel method then the Task is marked to receive a CancelledError exception the next time it runs on the event loop. If you don’t handle the exception the Task simply dies silently. If you have any resources to close then you have to handle the exception to perform the cleanup. You can even opt to ignore the CancelledError exception altogether.

For example:

import uasyncio
async def test1(msg):
    try:
        await uasyncio.sleep(0)       
    except:
        pass
    print(msg)
    return msg
async def main():
    t1 = uasyncio.create_task(test1("one"))
    await uasyncio.sleep(0)
    t1.cancel()
    print("Hello Coroutine World")
    await uasyncio.sleep(0)
uasyncio.run(main())

In this case the try suppresses the exception and everything works as if cancel had not been called. If you remove the try/except then you don't see one displayed as test1 is canceled.

Dealing with Exceptions

The fact that a Task is modeled on a Future should immediately tell you how to handle exceptions in asynchronous programs. The Task either returns a result or an exception object and by default the Exception object is used to raise the exception in the calling coroutine. For example:

import uasyncio
async def test(msg):
    print(msg)
    raise Exception("Test exception")
async def main():
    t1 = uasyncio.create_task(test("one"))
    try:
        await t1
    except:
        print("an exception has occurred")
    print("Hello Coroutine World")
    await uasyncio.sleep(0)
uasyncio.run(main())

In this case test raises an exception as soon as it is called. This is returned to the calling coroutine and raised again by the await operation.

If you want to access the exception object and handle it manually you will have to use one of the gather methods as await always consumes the Task and hence raises the exception whereas gather can return all results including exceptions. For example:

import uasyncio
async def test(msg):
    print(msg)
    raise Exception("Test exception")
async def main():
    t1=uasyncio.create_task(test("one"))
    result=None
    try:
        result = await uasyncio.gather(
t1,return_exceptions=True) except: print("an exception has occurred") print("Hello Coroutine World") print(result) await uasyncio.sleep(0) uasyncio.run(main())

displays:

one
Hello Coroutine World
[Exception('Test exception',)]

You can see that now you have the Exception object and you can choose to do what you like with it or raise it when you are ready.

Finally if you don’t await a Task then any exceptions are ignored, just as any results are ignored. The Task fails silently, just as it succeeds silently if it is not awaited.



Last Updated ( Tuesday, 10 September 2024 )