How can I wrap a synchronous function in an async coroutine?
Eventually I found an answer in this thread. The method I was looking for is run_in_executor. This allows a synchronous function to be run asynchronously without blocking an event loop.
In the sleep
example I posted above, it might look like this:
import asynciofrom time import sleepasync def sleep_async(loop, delay): # None uses the default executor (ThreadPoolExecutor) await loop.run_in_executor(None, sleep, delay) return 'I slept asynchronously'
Also see the following answer -> How do we call a normal function where a coroutine is expected?
You can use a decorator to wrap the sync version to an async version.
import timefrom functools import wraps, partialdef wrap(func): @wraps(func) async def run(*args, loop=None, executor=None, **kwargs): if loop is None: loop = asyncio.get_event_loop() pfunc = partial(func, *args, **kwargs) return await loop.run_in_executor(executor, pfunc) return run@wrapdef sleep_async(delay): time.sleep(delay) return 'I slept asynchronously'
or use the aioify library
% pip install aioify
then
@aioifydef sleep_async(delay): pass
The decorator would be useful for this case and run your blocking function in another thread.
import asynciofrom concurrent.futures import ThreadPoolExecutorfrom functools import wraps, partialfrom typing import Unionclass to_async: def __init__(self, *, executor: Optional[ThreadPoolExecutor]=None): self.executor = executor def __call__(self, blocking): @wraps(blocking) async def wrapper(*args, **kwargs): loop = asyncio.get_event_loop() if not self.executor: self.executor = ThreadPoolExecutor() func = partial(blocking, *args, **kwargs) return await loop.run_in_executor(self.executor,func) return wrapper@to_async(executor=None)def sync(*args, **kwargs): print(args, kwargs) asyncio.run(sync("hello", "world", result=True))