I want to write a bluetooth script with the help of bleak and encountered some issues with asyncio and while loops. I tried to create a minimal working environment in order to explain my problem:
In the while loop I'm awaiting the result of match_command(). This is a problem if I'm starting a new loop in this function as it will never terminate. It seems that asyncio.sleep() sets the whole main task to sleep instead of only periodic_loop().
The command line is supposed to ask for an user input. If it is "0", a periodic task is supposed to start. But the user is supposed to still be able to use the command line and enter a new input.
import asyncio
from aioconsole import ainput
stop_periodic = 0
stop_main = 0
class Commander:
def __init__(self, counter: int):
self.counter = counter
async def get_command(self):
command = await ainput(f"Instance {self.counter}\n0: periodic; 1: hi; 2: stop periodic; 3: stop "
f"main\nEnter command: ")
await match_command(command)
async def periodic_loop():
i = 0
print("periodic loop")
while stop_periodic == 0:
print(f"Iteration {i}")
i += 1
await asyncio.sleep(2)
async def match_command(command):
global stop_periodic, stop_main
match command:
case "0":
stop_periodic = 0
await periodic_loop()
case "1":
print("do something")
case "2":
stop_periodic = 1
case "3":
stop_main = 1
async def main():
i = 1
new_instance = Commander(0)
while stop_main == 0:
await new_instance.get_command()
if __name__ == '__main__':
asyncio.run(main())
I'm a bit lost. I tried asyncio.createTask(), TaskGroups and meddled with futures but I'm simply not getting anywhere. The most promising option was something along
async with asyncio.TaskGroup() as tg:
if command == "0":
task1 = tg.create_task(match_command(command))
new_instance = Commander(i)
task2 = tg.create_task(new_instance.get_command())
else:
await match_command(command)
which allowed me to create a new Command instance if the user input was "0". However, that still meant there could only be one periodic task. What if I wanted 4 of them?
So I guess if I have a periodic task, I need to create a new Commander instance in order to have a new task to which the program can switch while asyncio.sleep() is putting the periodic task to sleep. If I try to do it for more than 2 instances though it gets really nested in my code and I'm lacking the skill to have a clean solution.
Do you have any suggetions? Thanks a lot!
Your problem is this part:
match command:
case "0":
stop_periodic = 0
await periodic_loop()
Whenever you call async tasks inline using the await
expression, the current code will stop and wait for the awaited expression to resolve - just like ordinary synchronous function calls. This ensures one write code in order which is able to run concurrently with other tasks. So when the await periodic_loop()
line is reached, the code in periodic_loop is executed, and never returns, which means the first get_command
call in main
will never return as well, as this is an inner call to that one.
What you need to do is to create a new task on the event loop to execute the periodic_loop
, and, instead of awaiting for it, just keep track of it in some data structure (so that you can cancel/check its status later, and avoid that it get lost in limbo)
so - changing these parts should suffice, and it is trivial to add code to cancel your running loops upon a specific user entry later on.
import asyncio
from aioconsole import ainput
stop_periodic = 0
stop_main = 0
running_tasks = set()
...
async def match_command(command):
global stop_periodic, stop_main
match command:
case "0":
stop_periodic = 0
running_tasks.add(asyncio.create_task(periodic_loop()))
case "1":
...
(Note that I used a set for keeping a reference to the task, but you can use whatever data structure you want, even a single global variable, or a dictionary. Actually, if you want one single instance of the periodic_loop
to be running at a given time, those are easier to check for an already running one)