Search code examples
python-3.xasynchronouspython-asyncio

Run and wait for asynchronous function from a synchronous one using Python asyncio


In my code I have a class with properties, that occasionally need to run asynchronous code. Sometimes I need to access the property from asynchronous function, sometimes from synchronous - that's why I don't want my properties to be asynchronous. Besides, I have an impression that asynchronous properties in general is a code smell. Correct me if I'm wrong.

I have a problem with executing the asynchronous method from the synchronous property and blocking the further execution until the asynchronous method will finish.

Here is a sample code:

import asyncio


async def main():
    print('entering main')
    synchronous_property()
    print('exiting main')


def synchronous_property():
    print('entering synchronous_property')
    loop = asyncio.get_event_loop()
    try:
        # this will raise an exception, so I catch it and ignore
        loop.run_until_complete(asynchronous())
    except RuntimeError:
        pass
    print('exiting synchronous_property')


async def asynchronous():
    print('entering asynchronous')
    print('exiting asynchronous')


asyncio.run(main())

Its output:

entering main
entering synchronous_property
exiting synchronous_property
exiting main
entering asynchronous
exiting asynchronous

First, the RuntimeError capturing seems wrong, but if I won't do that, I'll get RuntimeError: This event loop is already running exception.

Second, the asynchronous() function is executed last, after the synchronous one finish. I want to do some processing on the data set by asynchronous method so I need to wait for it to finish. If I'll add await asyncio.sleep(0) after calling synchronous_property(), it will call asynchronous() before main() finish, but it doesn't help me. I need to run asynchronous() before synchronous_property() finish.

What am I missing? I'm running python 3.7.


Solution

  • Asyncio is really insistent on not allowing nested event loops, by design. However, you can always run another event loop in a different thread. Here is a variant that uses a thread pool to avoid having to create a new thread each time around:

    import asyncio, concurrent.futures
    
    async def main():
        print('entering main')
        synchronous_property()
        print('exiting main')
    
    pool = concurrent.futures.ThreadPoolExecutor()
    
    def synchronous_property():
        print('entering synchronous_property')
        result = pool.submit(asyncio.run, asynchronous()).result()
        print('exiting synchronous_property', result)
    
    async def asynchronous():
        print('entering asynchronous')
        await asyncio.sleep(1)
        print('exiting asynchronous')
        return 42
    
    asyncio.run(main())
    

    This code creates a new event loop on each sync→async boundary, so don't expect high performance if you're doing that a lot. It could be improved by creating only one event loop per thread using asyncio.new_event_loop, and caching it in a thread-local variable.