Execute async callback from synchronous code immediately - python

Problem
I have a library which currently has no async support and needs to be called from async code. The async code calls into the library through a handler (handler function in the code below). While the handler executed, the library periodically calls a callback (callback_wrapper) to report progress.
The synchronous handler is executed in a ThreadPoolExecutor in order for the main event loop to be able to process further events while the handler is running.
What happens is that the synchronous callback is executed immediately, but the async callback is only executed after the main handler has executed. The desired result is the async callbacks to be executed immediately.
I guess the event loop is blocked at the run_in_executor call, but I am not sure how to resolve this.
Code
import asyncio
import time
from concurrent.futures.thread import ThreadPoolExecutor
loop = asyncio.get_event_loop()
def handler():
print('handler started')
callback_wrapper()
time.sleep(1)
print('handler stopped')
async def callback():
print('callback')
def callback_wrapper():
print('callback wrapper started')
asyncio.ensure_future(callback(), loop=loop)
print('callback wrapper stopped')
async def main():
handler()
with ThreadPoolExecutor() as pool:
async def thread_handler():
await loop.run_in_executor(pool, handler)
loop.run_until_complete(main())
Output
handler started
callback wrapper started
callback wrapper stopped
handler stopped
callback
Desired Output
handler started
callback wrapper started
callback
callback wrapper stopped
handler stopped

thanks to #user4815162342's input, I came up the the following solution:
import asyncio
import time
from concurrent.futures.thread import ThreadPoolExecutor
loop = asyncio.get_event_loop()
def handler():
print('handler started')
callback_wrapper()
time.sleep(1)
print('handler stopped')
async def callback():
print('callback')
def callback_wrapper():
print('callback wrapper started')
asyncio.run_coroutine_threadsafe(callback(), loop).result()
print('callback wrapper stopped')
async def main():
await thread_handler()
with ThreadPoolExecutor() as pool:
async def thread_handler():
await loop.run_in_executor(pool, handler)
loop.run_until_complete(main())
which produces the desired result:
handler started
callback wrapper started
callback
callback wrapper stopped
handler stopped

Related

Adding a coroutine to a running asyncio loop

First of all, just want to clarify that I already tried what this thread answers how to add a coroutine to a running asyncio loop? no success.
Context: I have a coroutine listening to a websocket that gives orders to my script to run and keep running other coroutines.
Problem: When I run my code the first coroutine inside SomeClass run() method (sa.run() line) is never awaited.
Main file:
import websockets, asyncio
async def main():
async with websockets.connect(XXX) as serv:
print('Connected to server')
sa = SomeClass(XXX, XXX, serv)
sa.run() # Run in loop handler
asyncio.run(main())
SomeClass definition:
class AsyncLoopThread(Thread):
def __init__(self):
super().__init__(daemon=True)
self.loop = asyncio.new_event_loop()
def run(self):
asyncio.set_event_loop(self.loop)
self.loop.run_forever()
class SomeClass:
def __init__(self, params) -> None:
#Some initiation simple code
self.loop_handler = AsyncLoopThread()
self.loop_handler.start()
async def get_obj_generator(self) -> Generator:
async with websockets.connect(self.some_server_uri) as ws_some_server:
while True:
some_bytes = await ws_some_server.recv()
some_object = pickle.loads(some_bytes)
print('something received.')
yield some_object
async def on_obj_update(self):
obj_generator = self.get_obj_generator()
async for obj in obj_generator:
opportunities = await self.search_opportunities(obj)
for opportunity in opportunities:
#opportunity.execute() is a async def corroutine with some await lines
asyncio.run_coroutine_threadsafe(opportunity.execute(), self.loop_handler.loop)
def run(self):
import time
print(time.time())
asyncio.run_coroutine_threadsafe(self.on_obj_update(), self.loop_handler.loop)
print(time.time())
Output console:
Connected to valuation server...
1658947843.412255
1658947843.413252

python asyncio.Event.wait() not responding to event.set()

The plan is to have several IO routines running "concurrently" (specifically on a Raspberry Pi, manipulating IO pins and running an SPI interface at the same time). I try to use asyncio to make this happen. However, my simple try-out refuses to run.
This is a reduced version of the code, leaving out the IO pin details:
"""\
Reduced problem representation:
this won't run because GPIO details have been left out
"""
import RPi.GPIO as gpio
import asyncio
GPIO_PB = 12 # Define pushbutton channel
async def payload():
""" Provides some payload sequence using asyncio.sleep() """
#Payload action
await asyncio.sleep(1)
#Payload action
await asyncio.sleep(1)
class IOEvent(asyncio.locks.Event):
"""\
Create an Event for asyncio, fired by a callback from GPIO
The callback must take a single parameter: a gpio channel number
"""
def __init__(self, ioChannel, loop):
super().__init__(loop = loop)
self.io = ioChannel
def get_callback(self):
"The callback is a closure that knows self when called"
def callback( ch ):
print("callback for channel {}".format(ch))
if ch == self.io and not self.is_set():
print(repr(self))
self.set()
print(repr(self))
return callback
async def Worker(loop, event):
print("Entering Worker: {}".format(repr(loop)))
while loop.is_running():
print("Worker waiting for {}".format(repr(event)))
await event.wait()
print("Worker has event")
event.clear()
await payload()
print("payload ended")
loop = asyncio.get_event_loop()
# Create an event for the button
pb_event = IOEvent( GPIO_PB, loop)
# register the pushbutton's callback
# Pushing the button calls this callback function
gpio.add_event_callback( GPIO_PB, pb_event.get_callback() )
try:
asyncio.ensure_future(Worker(loop, pb_event))
loop.run_forever()
except KeyboardInterrupt:
pass
finally:
print("Closing Loop")
loop.stop()
loop.close()
The output I get is like this:
Entering Worker: <_UnixSelectorEventLoop running=True closed=False debug=False>
Worker waiting for <__main__.IOEvent object at 0x76a2a950 [unset]>
callback for channel 12
<__main__.IOEvent object at 0x76a2a950 [unset,waiters:1]>
<__main__.IOEvent object at 0x76a2a950 [set,waiters:1]>
callback for channel 12
These lines show the pushbutton repeatedly and correctly firing its callback routine. The first time it calls the set() funtion as expected. The event used for the wait() call and the set() call are the same. But the message "Worker has event", after the await event.wait() call never appears.
I looked at PyQt5 and asyncio: yield from never finishes, but I do not see any other loops than the default loop.
Why does wait() never return? How could I find out?
Callbacks set by add_event_callback are called from a different thread, as indicated by them being called automatically "in the background". This means that you can't call set on an asyncio.Event directly from a gpio callback, since asyncio classes are not thread-safe.
To wake up an asyncio.Event from a different thread, you can pass event.set to loop.call_soon_threadsafe. In your case, you would change:
self.set()
to:
self._loop.call_soon_threadsafe(self.set)

Asyncio exception handler: not getting called until event loop thread stopped

I am setting an exception handler on my asyncio event loop. However, it doesn't seem to be called until the event loop thread is stopped. For example, consider this code:
def exception_handler(loop, context):
print('Exception handler called')
loop = asyncio.get_event_loop()
loop.set_exception_handler(exception_handler)
thread = Thread(target=loop.run_forever)
thread.start()
async def run():
raise RuntimeError()
asyncio.run_coroutine_threadsafe(run(), loop)
loop.call_soon_threadsafe(loop.stop, loop)
thread.join()
This code prints "Exception handler called", as we might expect. However, if I remove the line that shuts-down the event loop (loop.call_soon_threadsafe(loop.stop, loop)) it no longer prints anything.
I have a few questions about this:
Am I doing something wrong here?
Does anyone know if this is the intended behaviour of asyncio exception handlers? I can't find anything that documents this, and it seems a little strange to me.
I'd quite like to have a long-running event loop that logs errors happening in its coroutines, so the current behaviour seems problematic for me.
There are a few problems in the code above:
stop() does not need a parameter
The program ends before the coroutine is executed (stop() was called before it).
Here is the fixed code (without exceptions and the exception handler):
import asyncio
from threading import Thread
async def coro():
print("in coro")
return 42
loop = asyncio.get_event_loop()
thread = Thread(target=loop.run_forever)
thread.start()
fut = asyncio.run_coroutine_threadsafe(coro(), loop)
print(fut.result())
loop.call_soon_threadsafe(loop.stop)
thread.join()
call_soon_threadsafe() returns a future object which holds the exception (it does not get to the default exception handler):
import asyncio
from pprint import pprint
from threading import Thread
def exception_handler(loop, context):
print('Exception handler called')
pprint(context)
loop = asyncio.get_event_loop()
loop.set_exception_handler(exception_handler)
thread = Thread(target=loop.run_forever)
thread.start()
async def coro():
print("coro")
raise RuntimeError("BOOM!")
fut = asyncio.run_coroutine_threadsafe(coro(), loop)
try:
print("success:", fut.result())
except:
print("exception:", fut.exception())
loop.call_soon_threadsafe(loop.stop)
thread.join()
However, coroutines that are called using create_task() or ensure_future() will call the exception_handler:
async def coro2():
print("coro2")
raise RuntimeError("BOOM2!")
async def coro():
loop.create_task(coro2())
print("coro")
raise RuntimeError("BOOM!")
You can use this to create a small wrapper:
async def boom(x):
print("boom", x)
raise RuntimeError("BOOM!")
async def call_later(coro, *args, **kwargs):
loop.create_task(coro(*args, **kwargs))
return "ok"
fut = asyncio.run_coroutine_threadsafe(call_later(boom, 7), loop)
However, you should probably consider using a Queue to communicate with your thread instead.

Scheduling an asyncio coroutine from another thread

I try to schedule an asyncio coroutine from another thread using create_task(). The problem is that the coroutine is not called, at least not in reasonable amount of time.
Is there are way to wake up the event loop or at least specify a shorter timeout?
#!/usr/bin/python3
import asyncio, threading
event_loop = None
#asyncio.coroutine
def coroutine():
print("coroutine called")
def scheduler():
print("scheduling...")
event_loop.create_task(coroutine())
threading.Timer(2, scheduler).start()
def main():
global event_loop
threading.Timer(2, scheduler).start()
event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(event_loop)
event_loop.run_forever()
main()
Output:
scheduling...
scheduling...
scheduling...
scheduling...
According to the documentation of Task "this class is not thread safe". So scheduling from another thread is not expected to work.
I found two solutions for this based on the answers and comments here.
#wind85 answer: directly replacing the create_task line call with asyncio.run_coroutine_threadsafe(coroutine(), event_loop) call. Requires Python 3.5.1.
Use call_soon_threadsafe to schedule a callback, which then creates the task:
def do_create_task():
eventLoop.create_task(coroutine())
def scheduler():
eventLoop.call_soon_threadsafe(do_create_task)
Here we go this shuold work. It's a port. Try it out since I have the latest version, I can't really assure you it will work.
#!/usr/bin/python3
import concurrent.futures
import threading, asyncio
from asyncio import coroutines, futures
def run_coroutine_threadsafe_my(coro, loop):
"""Submit a coroutine object to a given event loop.
Return a concurrent.futures.Future to access the result.
"""
if not coroutines.iscoroutine(coro):
raise TypeError('A coroutine object is required')
future = concurrent.futures.Future()
def callback():
try:
futures._chain_future(asyncio.ensure_future(coro, loop=loop), future)
except Exception as exc:
if future.set_running_or_notify_cancel():
future.set_exception(exc)
raise
loop.call_soon_threadsafe(callback)
return future
event_loop = None
#asyncio.coroutine
async def coro():
print("coroutine called")
def scheduler():
print("scheduling...")
run_coroutine_threadsafe_my(coro(),event_loop)
threading.Timer(2, scheduler).start()
def main():
global event_loop
threading.Timer(2, scheduler).start()
event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(event_loop)
event_loop.run_forever()
main()

python asyncio, how to create and cancel tasks from another thread

I have a python multi-threaded application. I want to run an asyncio loop in a thread and post calbacks and coroutines to it from another thread. Should be easy but I cannot get my head around the asyncio stuff.
I came up to the following solution which does half of what I want, feel free to comment on anything:
import asyncio
from threading import Thread
class B(Thread):
def __init__(self):
Thread.__init__(self)
self.loop = None
def run(self):
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop) #why do I need that??
self.loop.run_forever()
def stop(self):
self.loop.call_soon_threadsafe(self.loop.stop)
def add_task(self, coro):
"""this method should return a task object, that I
can cancel, not a handle"""
f = functools.partial(self.loop.create_task, coro)
return self.loop.call_soon_threadsafe(f)
def cancel_task(self, xx):
#no idea
#asyncio.coroutine
def test():
while True:
print("running")
yield from asyncio.sleep(1)
b.start()
time.sleep(1) #need to wait for loop to start
t = b.add_task(test())
time.sleep(10)
#here the program runs fine but how can I cancel the task?
b.stop()
So starting and stoping the loop works fine. I thought about creating task using create_task, but that method is not threadsafe so I wrapped it in call_soon_threadsafe. But I would like to be able to get the task object in order to be able to cancel the task. I could do a complicated stuff using Future and Condition, but there must be a simplier way, isnt'it?
I think you may need to make your add_task method aware of whether or not its being called from a thread other than the event loop's. That way, if it's being called from the same thread, you can just call asyncio.async directly, otherwise, it can do some extra work to pass the task from the loop's thread to the calling thread. Here's an example:
import time
import asyncio
import functools
from threading import Thread, current_thread, Event
from concurrent.futures import Future
class B(Thread):
def __init__(self, start_event):
Thread.__init__(self)
self.loop = None
self.tid = None
self.event = start_event
def run(self):
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.tid = current_thread()
self.loop.call_soon(self.event.set)
self.loop.run_forever()
def stop(self):
self.loop.call_soon_threadsafe(self.loop.stop)
def add_task(self, coro):
"""this method should return a task object, that I
can cancel, not a handle"""
def _async_add(func, fut):
try:
ret = func()
fut.set_result(ret)
except Exception as e:
fut.set_exception(e)
f = functools.partial(asyncio.async, coro, loop=self.loop)
if current_thread() == self.tid:
return f() # We can call directly if we're not going between threads.
else:
# We're in a non-event loop thread so we use a Future
# to get the task from the event loop thread once
# it's ready.
fut = Future()
self.loop.call_soon_threadsafe(_async_add, f, fut)
return fut.result()
def cancel_task(self, task):
self.loop.call_soon_threadsafe(task.cancel)
#asyncio.coroutine
def test():
while True:
print("running")
yield from asyncio.sleep(1)
event = Event()
b = B(event)
b.start()
event.wait() # Let the loop's thread signal us, rather than sleeping
t = b.add_task(test()) # This is a real task
time.sleep(10)
b.stop()
First, we save the thread id of the event loop in the run method, so we can figure out if calls to add_task are coming from other threads later. If add_task is called from a non-event loop thread, we use call_soon_threadsafe to call a function that will both schedule the coroutine, and then use a concurrent.futures.Future to pass the task back to the calling thread, which waits on the result of the Future.
A note on cancelling a task: You when you call cancel on a Task, a CancelledError will be raised in the coroutine the next time the event loop runs. This means that the coroutine that the Task is wrapping will aborted due to the exception the next time it hit a yield point - unless the coroutine catches the CancelledError and prevents itself from aborting. Also note that this only works if the function being wrapped is actually an interruptible coroutine; an asyncio.Future returned by BaseEventLoop.run_in_executor, for example, can't really be cancelled, because it's actually wrapped around a concurrent.futures.Future, and those can't be cancelled once their underlying function actually starts executing. In those cases, the asyncio.Future will say its cancelled, but the function actually running in the executor will continue to run.
Edit: Updated the first example to use concurrent.futures.Future, instead of a queue.Queue, per Andrew Svetlov's suggestion.
Note: asyncio.async is deprecated since version 3.4.4 use asyncio.ensure_future instead.
You do everything right.
For task stopping make method
class B(Thread):
# ...
def cancel(self, task):
self.loop.call_soon_threadsafe(task.cancel)
BTW you have to setup an event loop for the created thread explicitly by
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
because asyncio creates implicit event loop only for main thread.
just for reference here it the code I finally implemented based on the the help I got on this site, it is simpler since I did not need all features. thanks again!
import asyncio
from threading import Thread
from concurrent.futures import Future
import functools
class B(Thread):
def __init__(self):
Thread.__init__(self)
self.loop = None
def run(self):
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.loop.run_forever()
def stop(self):
self.loop.call_soon_threadsafe(self.loop.stop)
def _add_task(self, future, coro):
task = self.loop.create_task(coro)
future.set_result(task)
def add_task(self, coro):
future = Future()
p = functools.partial(self._add_task, future, coro)
self.loop.call_soon_threadsafe(p)
return future.result() #block until result is available
def cancel(self, task):
self.loop.call_soon_threadsafe(task.cancel)
Since version 3.4.4 asyncio provides a function called run_coroutine_threadsafe to submit a coroutine object from a thread to an event loop. It returns a concurrent.futures.Future to access the result or cancel the task.
Using your example:
#asyncio.coroutine
def test(loop):
try:
while True:
print("Running")
yield from asyncio.sleep(1, loop=loop)
except asyncio.CancelledError:
print("Cancelled")
loop.stop()
raise
loop = asyncio.new_event_loop()
thread = threading.Thread(target=loop.run_forever)
future = asyncio.run_coroutine_threadsafe(test(loop), loop)
thread.start()
time.sleep(5)
future.cancel()
thread.join()

Categories

Resources