pythonpython-asyncionest-asyncio

Correct use/constraints of use, of nest_asyncio?


I'm working on making a previously sync webserver as sync one. Most of my functions are sync and I'd like to simply make async calls from existing code to avoid async creep. nest_asyncio appears to allow this by making run_until_complete re-entrant.

However, while this works for 1 reentrant call, I get a deadlock with two:

import asyncio
import functools
import time
from random import random
import nest_asyncio
nest_asyncio.apply()

def sync(corot, loop=None):
    """
    Make a synchronous function from an asynchronous one.
    :param corot:
    :return:
    """
    if loop is None:
        loop = asyncio.get_event_loop()
    result, = loop.run_until_complete(asyncio.gather(corot))
    return result

async def sync_to_corountine(func, *args, **kw):
    """
    Make a coroutine from a synchronous function.
    """
    try:
        return func(*args, *kw)
    finally:
        # every async needs an await.
        await asyncio.sleep(0)




def main():
    async def background(timeout):
        await asyncio.sleep(timeout)
        print(f"Background: {timeout}")

    loop = asyncio.get_event_loop()
    # Run some background work to check we are never blocked
    bg_tasks = [
        loop.create_task(background(i))
        for i in range(10)
    ]



    async def long_running_async_task(result):
        # Simulate slow IO
        print(f"...START long_running_async_task [{result}]")
        await asyncio.sleep(1)
        print(f"...END   long_running_async_task [{result}]")
        return result

    def sync_function_with_async_dependency(result):
        print(f"...START sync_function_with_async_dependency [{result}]")
        result = sync(long_running_async_task(result), loop=loop)
        print(f"...END   sync_function_with_async_dependency [{result}]")
        return result

    # Call sync_function_with_async_dependency
    # One reentrant task is OK
    # Multiple reentrant tasks=>fails to exit
    n = 2
    for i in range(n):
        bg_tasks.append(sync_to_corountine(sync_function_with_async_dependency, i))
    # for i in range(n):
    #     bg_tasks.append(long_running_async_task(i))

    # OK
    # bg_tasks.append(long_running_async_task(123))
    # bg_tasks.append(long_running_async_task(456))

    task = asyncio.gather(*bg_tasks)  # , loop=loop)
    loop.run_until_complete(task)

if __name__ == '__main__':
    main()

...START sync_function_with_async_dependency [0]
...START sync_function_with_async_dependency [1]
Background: 0
...START long_running_async_task [0]
...START long_running_async_task [1]
Background: 1
...END   long_running_async_task [0]
...END   long_running_async_task [1]
...END   sync_function_with_async_dependency [1]
Background: 2
Background: 3
Background: 4

... we are missing

...END   sync_function_with_async_dependency [0]

Am I using nest_asyncio correctly?


Solution

  • You want to use gevent if you're going to nest async calls inside synchronous calls, otherwise it can lead to starvation issues.

    The whole design of asyncio is async routines call async routines, but sync routines can not (except via run). They do this as they feel that anything else will lead to overly complex code that is difficult to follow. Probably bugs in asyncio are harder to fix as well.

    The monkey patch nest_async tries to break that design principle, but it doesn't do a very good job of it as nested runs will not give time to tasks scheduled outside the nested run. This can potentially leading to starvation if you spend too much time on code inside the nested run.

    I tried to explain this to the author, but he's still struggling:

    https://github.com/erdewit/nest_asyncio/issues/36