Search code examples
pythondjangopython-asynciodjango-channels

Django — async_to_sync vs asyncio.run


We can use both functions to run any async function synchronously:

import asyncio
from asgiref.sync import async_to_sync

asyncio.run(asyncio.sleep(1))
async_to_sync(asyncio.sleep)(1)

What is the difference? Can we always use asyncio.run instead of async_to_sync?


Solution

  • Differences

    1. They have different purposes. async_to_sync turns an awaitable into a synchronous callable, and asyncio.run executes a coroutine and return the result.

    2. According to documentation, a callable from async_to_sync works in a subthread.

    3. async_to_sync does not create an event loop per-thread in case when you're inside synchronous code which is produced by sync_to_async and running inside asynchronous code. It reuses a loop of asynchronous code. Let's take an example:

    import asyncio
    from asgiref.sync import async_to_sync, sync_to_async
    
    async def running(n):
        return [await sync_to_async(sync)(i) for i in range(n)]
    
    def sync(n):
        # it will create a new loop for every call
        return asyncio.run(from_sync(n))
    
    async def from_sync(n):
        return n
    
    print("Result:", asyncio.run(running(3)))
    

    This one will run 4 loops: 1 to call running and 3 to call from_sync.

    If we use async_to_sync instead of asyncio.run inside sync we will reduce the number of loops to 1 to call running.

    To see it you can wrap new_event_loop function:

    def print_deco(fn, msg):
        def inner():
            res = fn()
            print(msg, res)
            return res
        return inner
    p = asyncio.get_event_loop_policy()
    p.new_event_loop = print_deco(p.new_event_loop, "NEW EVENT LOOP:")
    

    You can find a detailed explanation in this post.