asyncio: Calling .run() within a task does not behave as per CPython
Port, board and/or hardware
Unix build
MicroPython version
MicroPython v1.23.0-preview.72.g4a2e510a8.dirty on 2024-02-04; linux [GCC 11.4.0] version
Reproduction
import asyncio
async def foo(s):
while True:
print(f"Task {s}")
await asyncio.sleep(1)
async def main():
asyncio.create_task(foo(1))
await asyncio.sleep(2)
asyncio.run(foo(2)) # CPython throws a RuntimeError
await asyncio.sleep(2)
asyncio.create_task(foo(1))
await asyncio.sleep(5)
print("Done") # never happens in MP
asyncio.run(main())
Expected behaviour
On CPython issuing run() in a running task throws a RuntimeError:
Python 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import rats17
Task 1
Task 1
Task 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/adminpete/rats17.py", line 17, in <module>
asyncio.run(main())
File "/usr/lib/python3.10/asyncio/runners.py", line 44, in run
return loop.run_until_complete(main)
File "/usr/lib/python3.10/asyncio/base_events.py", line 649, in run_until_complete
return future.result()
File "/home/adminpete/rats17.py", line 11, in main
asyncio.run(foo(2)) # CPython throws a RuntimeError
File "/usr/lib/python3.10/asyncio/runners.py", line 33, in run
raise RuntimeError(
RuntimeError: asyncio.run() cannot be called from a running event loop
>>>
Observed behaviour
The run() command appears to work, starting the second foo instance. However main never terminates.
MicroPython v1.23.0-preview.72.g4a2e510a8.dirty on 2024-02-04; linux [GCC 11.4.0] version
Use Ctrl-D to exit, Ctrl-E for paste mode
>>> import rats17
Task 1
Task 1
Task 1
Task 2
Task 1
Task 2
Task 1
Task 2
Task 1
Task 2
... continues forever
Additional Information
This arose from this issue.
A possible fix to asyncio.core.py is
def run(coro):
if cur_task is None:
return run_until_complete(create_task(coro))
else:
raise RuntimeError("asyncio.run() cannot be called from a running event loop")
Code of Conduct
Yes, I agree
extmod/uasyncio Discrepancy between behaviour and docs
The doc for the class Task states:
Task.cancel()
Cancel the task by injecting a CancelledError into it. The task may or may not ignore this exception.
The following script produces identical results in CPython 3.8 and MicroPython, with no exception occurring in bar(). The script runs correctly apart from this.
Is there in fact a way to detect cancellation within a task, or are the docs wrong?
try:
import uasyncio as asyncio
except ImportError:
import asyncio
async def bar():
try:
while True:
print('Bar running')
await asyncio.sleep(1)
except Exception as e:
print('bar stopped.', e) # Never happens (CPython or mp)
async def main():
t = asyncio.create_task(bar())
await asyncio.sleep(2)
t.cancel()
await asyncio.sleep(1)
asyncio.run(main())
Detecting cancellation or timeouts within a task is actually very useful e.g. for cleanup, if there is a way to achieve it.