I have a python application that handles remote control of a drone. I mainly employ traditional multithreading, but I have to support asynchronous programming too since some third party libs insist on using it. Currently, I have the following situation:
The current flying mission is temporarily halted, which runs a asynchronous while loop that constantly checks for a resume flag and the waits a few milliseconds using asyncio.sleep
. During that time, a websocket gets a command to adjust the drone's yaw angle. This starts a new thread which runs the appropriate drone code. However, this code is never executed, the while loop seems to be blocking all execution of further asynchronous code, despite the constant calls to asyncio.sleep
.
The following sscce illustrates the process:
import asyncio
import threading
import time
from concurrent.futures import ThreadPoolExecutor
class Drone():
def __init__(self):
self.ex = ThreadPoolExecutor(max_workers=5)
self.base_loop = asyncio.get_event_loop()
async def run(self):
print("Mission was temporarily halted, starting waiting loop...")
while True:
await asyncio.sleep(.2)
async def adjust_yaw(self):
print("Yaw angle adjusted.")
def sender(self):
time.sleep(2)
print("adjusting yaw angle...")
asyncio.run_coroutine_threadsafe(self.adjust_yaw(), self.base_loop)
if __name__ == '__main__':
drone = Drone()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
th = threading.Thread(target=drone.sender, name="MissionRunner", args=[])
th.start()
loop.run_until_complete(drone.run())
As you can see, "Yaw angle adjusted."
is never shown.
What would be the best way to unblock this loop?
It would be great if the changes could be limited to the sender
method. As already mentioned, I'm just including asyncio because I have to and I wouldn't want to make big adjustments in my working codebase just because of this one library.
Addendum
I tried jsbueno's answers, which work perfectly in the example provided. However, putting them into my actual code, the event loop still refuses to run anything in the sender method. I don't know which other parts in my code are relevant to this behaviour and showing all code involved would blow this thread out of proportion.
The only working solution so far is not to care. I don't memorize or set any loops, and in the sender method, I simply await the methods with whatever event loop is currently running. The relevant methods will throw errors in the form
RuntimeError: Task <Task pending coro=<main() running at /home/pi/mission/cpa/socket_listener.py:264> cb=[_run_until_complete_cb() at /usr/lib/python3.7/asyncio/base_events.py:158]> got Future <Future pending cb=[wrap_future.<locals>._call_check_cancel() at /home/pi/venv_mission/lib/python3.7/site-packages/aiogrpc/utils.py:52]> attached to a different loop
But before that, all necessary interactions with the drone are completed. So the program complains, but it works. An unsatisfying solution, but a solution nonetheless...
The only incorrect thing in this code is that you try to pick the running asyncio loop inside Drone's __init__
before there is an event loop running.
Just change the initialization order, and pass the loop where you will run your async tasks as a parameter instead:
import asyncio
import threading
import time
from concurrent.futures import ThreadPoolExecutor
class Drone():
def __init__(self, loop):
self.ex = ThreadPoolExecutor(max_workers=5)
self.base_loop = loop
# remainder of class body unchanged
...
if __name__ == '__main__':
# get a loop reference before instantiating the Drone:
loop = asyncio.new_event_loop()
drone = Drone(loop)
# this line is actually not needed:
asyncio.set_event_loop(loop)
th = threading.Thread(target=drone.sender, name="MissionRunner", args=[])
th.start()
loop.run_until_complete(drone.run())
Or, if you really want to change just the sender
message, you can call it with the explicit loop parameter, instead of holding it as a class attribute:
...
class Drone():
def __init__(self):
self.ex = ThreadPoolExecutor(max_workers=5)
# no loop attribute on the class!
...
def sender(self, loop):
time.sleep(2)
print("adjusting yaw angle...")
asyncio.run_coroutine_threadsafe(self.adjust_yaw(), loop)
if __name__ == '__main__':
drone = Drone()
loop = asyncio.new_event_loop()
th = threading.Thread(target=drone.sender, name="MissionRunner", args=[loop,])
th.start()
loop.run_until_complete(drone.run())