Thanks for the guidance. It's truly amazing that there is any support at all given the headroom available in the target environments, yes! This is bound to come with some compromises.
I was ready to explore various ways to do cancellation but thought I was stuck a step earlier than that. I had understood I couldn't detect the circumstances to
attempt cancellation, because I didn't have the means of monitoring multiple co-routines for exceptions they raise. I was wrong.
I had encountered a case where loop.run_forever() seemed to swallow the exceptions raised by its coroutines, and I understood therefore that loop.run_until_complete(coro) was the only way to monitor the results (including exceptions) from any coro, but therefore I was limited to monitoring exceptions from a single coro.
For this reason I was puzzling how to monitor exceptions from more than one coro, (as I had achieved with asyncio.gather() with the Task-oriented CPython API, to turn multiple tasks into a single task).
However, it seems like the exception-swallowing I encountered was to do with an except+pass clause in a coro which was too broad. In fact, monitoring for all exceptions from all coros
is the default behaviour of both loop.run_forever() and loop.run_until_complete() but I hadn't figured this out.
So after more experimentation I have come to believe two important new facts which make sense of everything...
- loop.run_forever() actually raises exceptions encountered when evaluating scheduled continuations within any of the coroutines previously scheduled with loop.create_task()
- loop.run_until_complete(coro) also raises exceptions arising in other coros than coro, but just terminates early when coro has a result (or exception).
That means I should be in a position to handle exceptions from all coros, and then trigger a cancellation.
As regards cancellation itself, the following example seems effective on ESP8266. It successfully restarts an asynchronous Russian roulette tournament when the first player wins...
Code: Select all
import uasyncio as asyncio
from uos import urandom
def random_uint8():
return int(urandom(1)[0])
class CancelledException(BaseException):
pass
class BangException(BaseException):
pass
async def russian_roulette(bulletPos = None):
if bulletPos is None:
bulletPos = random_uint8() % 6
cylinderPos = 0
while cylinderPos < 6:
await asyncio.sleep(random_uint8() / 100)
print("Bullet {} Cylinder {}. Pulling trigger...".format(bulletPos, cylinderPos))
if cylinderPos == bulletPos:
print("...Bang!")
raise BangException
else:
print("...click")
cylinderPos += 1
numPlayers = 10
loop = asyncio.get_event_loop()
def run():
coros = []
for count in range(numPlayers):
coro = russian_roulette()
coros.append(coro)
loop.create_task(coro)
# keep pulling trigger until a coro bangs
try:
loop.run_forever()
except BangException as e:
pass
# dispose of all coros (banged or not)
for coro in coros:
try:
coro.throw(CancelledException)
except StopIteration: # raised by already stopped coro?
pass
except CancelledException: # raised by all others?
pass
# help loop tidy up the exceptions
for coro in coros:
loop.run_until_complete(coro)
while True:
print("Players ready")
run()
print("Game Over: A player has died")