Search code examples
pythonpython-asyncio

How to call async function from sync funcion and get result, while a loop is already running


I have a asyncio running loop, and from the coroutine I'm calling a sync function, is there any way we can call and get result from an async function in a sync function tried below code, it is not working want to print output of hel() in i() without changing i() to async function is it possible, if yes how?

import asyncio

async def hel():
    return 4

def i():
    loop = asyncio.get_running_loop()
    x = asyncio.run_coroutine_threadsafe(hel(), loop)   ## need to change
    y = x.result()                                      ## this lines
    print(y)

async def h():
    i()

asyncio.run(h())

Solution

  • This is one of the most commonly asked type of question here. The tools to do this are in the standard library and require only a few lines of setup code. However, the result is not 100% robust and needs to be used with care. This is probably why it's not already a high-level function.

    The basic problem with running an async function from a sync function is that async functions contain await expressions. Await expressions pause the execution of the current task and allow the event loop to run other tasks. Therefore async functions (coroutines) have special properties that allow them to yield control and resume again where they left off. Sync functions cannot do this. So when your sync function calls an async function and that function encounters an await expression, what is supposed to happen? The sync function has no ability to yield and resume.

    A simple solution is to run the async function in another thread, with its own event loop. The calling thread blocks until the result is available. The async function behaves like a normal function, returning a value. The downside is that the async function now runs in another thread, which can cause all the well-known problems that come with threaded programming. For many cases this may not be an issue.

    This can be set up as follows. This is a complete script that can be imported anywhere in an application. The test code that runs in the if __name__ == "__main__" block is almost the same as the code in the original question.

    The thread is lazily initialized so it doesn't get created until it's used. It's a daemon thread so it will not keep your program from exiting.

    The solution doesn't care if there is a running event loop in the main thread.

    import asyncio
    import threading
    
    _loop = asyncio.new_event_loop()
    
    _thr = threading.Thread(target=_loop.run_forever, name="Async Runner",
                            daemon=True)
    
    # This will block the calling thread until the coroutine is finished.
    # Any exception that occurs in the coroutine is raised in the caller
    def run_async(coro):  # coro is a couroutine, see example
        if not _thr.is_alive():
            _thr.start()
        future = asyncio.run_coroutine_threadsafe(coro, _loop)
        return future.result()
    
    if __name__ == "__main__":
        async def hel():
            await asyncio.sleep(0.1)
            print("Running in thread", threading.current_thread())
            return 4
        
        def i():
            y = run_async(hel())
            print("Answer", y, threading.current_thread())
        
        async def h():
            i()
        
        asyncio.run(h())
    

    Output:

    Running in thread <Thread(Async Runner, started daemon 28816)>
    Answer 4 <_MainThread(MainThread, started 22100)>