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
Asyncio task scheduling issue when using run_until_completed
Port, board and/or hardware
any port with asyncio support
MicroPython version
micropython-1.25.0 running on win32 [MSC v.1916 64 bit]
Reproduction
The issue is linked to the fact that run_until_complete does not complete the processing of the awaited task before returning to hte caller. If other pending tasks are waiting it, they will be lost forever.
The code below will reproduce the issue.
- When the tasks are await-ed form an async function, everything works as expected (function
async_test) - When the tasks are started from a sync function using run_until_complete, scheduling is broken (function
sync_test)
This issue is specific to micropython asyncio implementation ofrun_until_completeand does not occur in CPython.
import sys, asyncio, random
def get_event_loop():
if sys.implementation.name == "micropython":
return asyncio.get_event_loop()
else:
try:
eventloop = asyncio.get_running_loop()
except RuntimeError:
eventloop = asyncio.new_event_loop()
return eventloop
class Worker:
def __init__(self):
self._eventLoop = None
self._tasks = []
def launchTask(self, asyncJob):
if self._eventLoop is None:
self._eventLoop = get_event_loop()
return self._eventLoop.create_task(asyncJob)
async def job(self, prerequisite, taskName):
if prerequisite:
await prerequisite
await asyncio.sleep(2)
print(taskName, "work completed")
def planTasks(self):
self._tasks.append(self.launchTask(self.job(None, 'task0')))
self._tasks.append(self.launchTask(self.job(self._tasks[0], 'task1')))
self._tasks.append(self.launchTask(self.job(self._tasks[1], 'task2')))
async def waitForTask(self, taskIdx):
return await self._tasks[taskIdx]
def syncWaitForTask(self, taskIdx):
return self._eventLoop.run_until_complete(self._tasks[taskIdx])
async def async_test():
print("--- Running the async test")
worker = Worker()
worker.planTasks()
await worker.waitForTask(0)
print("-> task0 done")
await worker.waitForTask(2)
print("-> task2 done")
def sync_test():
print("--- Running the sync test")
worker = Worker()
worker.planTasks()
worker.syncWaitForTask(0)
print("-> task0 done")
worker.syncWaitForTask(2)
print("-> task2 done")
asyncio.run(async_test())
sync_test()
Expected behaviour
--- Running the async test
task0 work completed
-> task0 done
task1 work completed
task2 work completed
-> task2 done
--- Running the sync test
task0 work completed
-> task0 done
task1 work completed
task2 work completed
-> task2 done
Observed behaviour
--- Running the async test
task0 work completed
-> task0 done
task1 work completed
task2 work completed
-> task2 done
--- Running the sync test
task0 work completed
-> task0 done
-> task2 done
Additional Information
I will submit a pull request with a suggested fix
Code of Conduct
Yes, I agree