Shock content: Coroutines for blocking code

Hi djangonauts! I have written this post exclusively for django forum in a hope you will appreciate it! Please forgive my bad English.

Below you will find innovative recipes for people who deploy their app as ASGI, but want the blocking I/O to remain first-class.

This is an alternative to what the official docs recommend. Things like the sync_to_async decorator or the new async friendly API - you ain’t gonna need them! By “friendly API” I mean

# async for obj in queryset:
#     ...
# obj = await MyModel.objects.aget()
# await obj.asave()

You ain’t gonna need it. I will show you another way that makes your blocking code first-class.

There are a number of reasons why the official approach makes blocking code awkward and even ineffective. Ineffective it will be if you use the new async-friendly API shown above, and jump back and forth from blocking thread to async one. Awkward - in case you use sync_to_async.

They say your sync views can be automatically adapted:

def your_view(request: ASGIRequest):
    # blocking code

However, the request here is an ASGIRequest, and it is not really intended to be consumed by blocking code.

So here I give you my great idea: to use coroutines for blocking code.

As many of you know, coroutines are just a syntactic sugar over generators. And you can perfectly have a coroutine containing blocking code:

async def weird_coroutine():
    time.sleep(1)
    return 'Have been weird for 1 second'

# Here is how we can launch it:
try:
    gen = weird_coroutine()
    gen.send(None)
except StopIteration:
    pass

So I propose making your views async functions and write mostly blocking code in it. What’s the profit? It’s in the fact that you will be able to run the async code too, that case being an exception rather than the rule.

You will be able to do stuff like

async def myview(request: ASGIRequest):
    await read_uploaded_file(request, filename='shock.jpg')

    # blocking code

Also, you will be able to use asyncio:

async def myview(request):
    # blocking code
    
    async with io:
        async with httpx.AsyncClient() as client:
            response = await client.get(url)
    
    # more blocking code

Also, you will be able to execute (blocking) operations in parallel:

async def myview(request):
    # blocking code
    
    async with parallel_ops:
        # do stuff
        await add_op
        # do other stuff
        await add_op
        # something else
    # all three are done
    
    # more blocking code

To sum up, this allows you to keep blocking code first-class while using an ASGI server.

Maybe you are now asking yourself if this is technically possible. It is! Here is a gist. All the blocking code is stripped from the coroutine and executed in a threadpool.

The old WSGI is of course a good option too. You might want to try ASGI in cases when you have a significant amount of non-database I/O - for example, if you make requests to other services. Here are the reasons why:

1. You can have a more predictable number of Python threads and
connections to database

If you make requests to other sites, your controllers will take considerably more time to execute. With WSGI, you can solve that problem by increasing the number of threads. Since every thread holds a database connection, the number of those will also rise. This is generally not a problem, although the optimal number of threads will no longer be defined by the “real” needs of your app and will be harder to choose.

2. With asyncio, you may get better concurrency for non-database I/O

This is kind of obvious. With WSGI, a request to a third party site holds an entire thread, while with asyncio, you can have a single thread with a lot of them (requests).

In case those 2 reasons make some sense to you, you might want to try ASGI.

1 Like