Skip to content

Commit

Permalink
Clarified async documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
adamchainz committed Mar 26, 2020
1 parent 6ef4c8a commit 4216225
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 112 deletions.
1 change: 0 additions & 1 deletion docs/spelling_wordlist
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ databrowse
datafile
dataset
datasets
datastores
datatype
datetimes
Debian
Expand Down
203 changes: 108 additions & 95 deletions docs/topics/async.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ Asynchronous support

Django has support for writing asynchronous ("async") views, along with an
entirely async-enabled request stack if you are running under
:doc:`ASGI </howto/deployment/asgi/index>` rather than WSGI. Async views will
still work under WSGI, but with performance penalties, and without the ability
to have efficient long-running requests.
:doc:`ASGI </howto/deployment/asgi/index>`. Async views will still work under
WSGI, but with performance penalties, and without the ability to have efficient
long-running requests.

We're still working on asynchronous support for the ORM and other parts of
Django; you can expect to see these in future releases. For now, you can use
the :func:`sync_to_async` adapter to interact with normal Django, as well as
use a whole range of Python asyncio libraries natively. See below for more
details.
We're still working on async support for the ORM and other parts of Django.
You can expect to see this in future releases. For now, you can use the
:func:`sync_to_async` adapter to interact with the sync parts of Django.
There is also a whole range of async-native Python libraries that you can
integrate with.

.. versionchanged:: 3.1

Expand All @@ -40,20 +40,21 @@ class-based view, this means making its ``__call__()`` method an ``async def``
coroutine, ensure you set the ``_is_coroutine`` attribute of the view
to ``asyncio.coroutines._is_coroutine`` so this function returns ``True``.

Under a WSGI server, asynchronous views will run in their own, one-off event
loop. This means that you can do things like parallel, async HTTP calls to APIs
without any issues, but you will not get the benefits of an asynchronous
request stack.
Under a WSGI server, async views will run in their own, one-off event loop.
This means you can use async features, like parallel async HTTP requests,
without any issues, but you will not get the benefits of an async stack.

If you want these benefits - which are mostly around the ability to service
hundreds of connections without using any Python threads (enabling slow
streaming, long-polling, and other exciting response types) - you will need to
deploy Django using :doc:`ASGI </howto/deployment/asgi/index>` instead.
The main benefits are the ability to service hundreds of connections without
using Python threads. This allows you to use slow streaming, long-polling, and
other exciting response types.

If you want to use these, you will need to deploy Django using
:doc:`ASGI </howto/deployment/asgi/index>` instead.

.. warning::

You will only get the benefits of a fully-asynchronous request stack if you
have *no synchronous middleware* loaded into your site; if there is a piece
have *no synchronous middleware* loaded into your site. If there is a piece
of synchronous middleware, then Django must use a thread per request to
safely emulate a synchronous environment for it.

Expand All @@ -63,22 +64,30 @@ deploy Django using :doc:`ASGI </howto/deployment/asgi/index>` instead.
on debug logging for the ``django.request`` logger and look for log
messages about *`"Synchronous middleware ... adapted"*.

In either ASGI or WSGI mode, though, you can safely use asynchronous support to
run code in parallel rather than serially, which is especially handy when
dealing with external APIs or datastores.
In both ASGI and WSGI mode, you can still safely use asynchronous support to
run code in parallel rather than serially. This is especially handy when
dealing with external APIs or data stores.

If you want to call a part of Django that is still synchronous (like the ORM)
you will need to wrap it in a :func:`sync_to_async` call, like this::
If you want to call a part of Django that is still synchronous, like the ORM,
you will need to wrap it in a :func:`sync_to_async` call. For example::

from asgiref.sync import sync_to_async

results = sync_to_async(MyModel.objects.get)(pk=123)
results = sync_to_async(Blog.objects.get)(pk=123)

You may find it easier to move any ORM code into its own function and call that
entire function using :func:`sync_to_async`. If you accidentally try to call
part of Django that is still synchronous-only from an async view, you will
trigger Django's :ref:`asynchronous safety protection <async-safety>` to
protect your data from corruption.
entire function using :func:`sync_to_async`. For example::

from asgiref.sync import sync_to_async

@sync_to_async
def get_blog(pk):
return Blog.objects.select_related('author').get(pk=pk)

If you accidentally try to call a part of Django that is still synchronous-only
from an async view, you will trigger Django's
:ref:`asynchronous safety protection <async-safety>` to protect your data from
corruption.

Performance
-----------
Expand All @@ -88,56 +97,56 @@ WSGI, or a traditional sync view under ASGI), Django must emulate the other
call style to allow your code to run. This context-switch causes a small
performance penalty of around a millisecond.

This is true of middleware as well, however. Django will attempt to minimize
the number of context-switches. If you have an ASGI server, but all your
middleware and views are synchronous, it will switch just once, before it
This is also true of middleware. Django will attempt to minimize the number of
context-switches between sync and async. If you have an ASGI server, but all
your middleware and views are synchronous, it will switch just once, before it
enters the middleware stack.

If, however, you put synchronous middleware between an ASGI server and an
However, if you put synchronous middleware between an ASGI server and an
asynchronous view, it will have to switch into sync mode for the middleware and
then back to asynchronous mode for the view, holding the synchronous thread
open for middleware exception propagation. This may not be noticeable, but bear
in mind that even adding a single piece of synchronous middleware can drag your
whole async project down to running with one thread per request, and the
associated performance penalties.

You should do your own performance testing to see what effect ASGI vs. WSGI has
on your code. In some cases, there may be a performance increase even for
purely-synchronous codebase under ASGI because the request-handling code is
still all running asynchronously. In general, though, you will only want to
enable ASGI mode if you have asynchronous code in your site.
then back to async mode for the view. Django will also hold the sync thread
open for middleware exception propagation. This may not be noticeable at first,
but adding this penalty of one thread per request can remove any async
performance advantage.

You should do your own performance testing to see what effect ASGI versus WSGI
has on your code. In some cases, there may be a performance increase even for
a purely synchronous codebase under ASGI because the request-handling code is
still all running asynchronously. In general you will only want to enable ASGI
mode if you have asynchronous code in your project.

.. _async-safety:

Async-safety
Async safety
============

Certain key parts of Django are not able to operate safely in an asynchronous
Certain key parts of Django are not able to operate safely in an async
environment, as they have global state that is not coroutine-aware. These parts
of Django are classified as "async-unsafe", and are protected from execution in
an asynchronous environment. The ORM is the main example, but there are other
parts that are also protected in this way.
an async environment. The ORM is the main example, but there are other parts
that are also protected in this way.

If you try to run any of these parts from a thread where there is a *running
event loop*, you will get a
:exc:`~django.core.exceptions.SynchronousOnlyOperation` error. Note that you
don't have to be inside an async function directly to have this error occur. If
you have called a synchronous function directly from an asynchronous function
without going through something like :func:`sync_to_async` or a threadpool,
then it can also occur, as your code is still running in an asynchronous
context.
you have called a sync function directly from an async function,
without using :func:`sync_to_async` or similar, then it can also occur. This is
because your code is still running in a thread with an active event loop, even
though it may not be declared as async code.

If you encounter this error, you should fix your code to not call the offending
code from an async context; instead, write your code that talks to async-unsafe
in its own, synchronous function, and call that using
:func:`asgiref.sync.sync_to_async`, or any other preferred way of running
synchronous code in its own thread.

If you are *absolutely* in dire need to run this code from an asynchronous
context - for example, it is being forced on you by an external environment,
and you are sure there is no chance of it being run concurrently (e.g. you are
in a Jupyter_ notebook), then you can disable the warning with the
``DJANGO_ALLOW_ASYNC_UNSAFE`` environment variable.
code from an async context. Instead, write your code that talks to async-unsafe
functions in its own, sync function, and call that using
:func:`asgiref.sync.sync_to_async` (or any other way of running sync code in
its own thread).

You may still be forced to run sync code from an async context. For example,
if the requirement is forced on you by an external environment, such as in a
Jupyter_ notebook. If you are sure there is no chance of the code being run
concurrently, and you *absolutely* need to run this sync code from an async
context, then you can disable the warning by setting the
``DJANGO_ALLOW_ASYNC_UNSAFE`` environment variable to any value.

.. warning::

Expand All @@ -147,18 +156,20 @@ in a Jupyter_ notebook), then you can disable the warning with the

If you need to do this from within Python, do that with ``os.environ``::

import os

os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"

.. _Jupyter: https://jupyter.org/

Async adapter functions
=======================

It is necessary to adapt the calling style when calling synchronous code from
an asynchronous context, or vice-versa. For this there are two adapter
functions, made available from the ``asgiref.sync`` package:
:func:`async_to_sync` and :func:`sync_to_async`. They are used to transition
between sync and async calling styles while preserving compatibility.
It is necessary to adapt the calling style when calling sync code from an async
context, or vice-versa. For this there are two adapter functions, from the
``asgiref.sync`` module: :func:`async_to_sync` and :func:`sync_to_async`. They
are used to transition between the calling styles while preserving
compatibility.

These adapter functions are widely used in Django. The `asgiref`_ package
itself is part of the Django project, and it is automatically installed as a
Expand All @@ -171,28 +182,31 @@ dependency when you install Django with ``pip``.

.. function:: async_to_sync(async_function, force_new_loop=False)

Wraps an asynchronous function and returns a synchronous function in its place.
Can be used as either a direct wrapper or a decorator::
Takes an async function and returns a sync function that wraps it. Can be used
as either a direct wrapper or a decorator::

from asgiref.sync import async_to_sync

sync_function = async_to_sync(async_function)
async def get_data(...):
...

sync_get_data = async_to_sync(get_data)

@async_to_sync
async def async_function(...):
async def get_other_data(...):
...

The asynchronous function is run in the event loop for the current thread, if
one is present. If there is no current event loop, a new event loop is spun up
specifically for the async function and shut down again once it completes. In
either situation, the async function will execute on a different thread to the
calling code.
The async function is run in the event loop for the current thread, if one is
present. If there is no current event loop, a new event loop is spun up
specifically for the single async invocation and shut down again once it
completes. In either situation, the async function will execute on a different
thread to the calling code.

Threadlocals and contextvars values are preserved across the boundary in both
directions.

:func:`async_to_sync` is essentially a more powerful version of the
:py:func:`asyncio.run` function available in Python's standard library. As well
:py:func:`asyncio.run` function in Python's standard library. As well
as ensuring threadlocals work, it also enables the ``thread_sensitive`` mode of
:func:`sync_to_async` when that wrapper is used below it.

Expand All @@ -201,8 +215,8 @@ as ensuring threadlocals work, it also enables the ``thread_sensitive`` mode of

.. function:: sync_to_async(sync_function, thread_sensitive=False)

Wraps a synchronous function and returns an asynchronous (awaitable) function
in its place. Can be used as either a direct wrapper or a decorator::
Takes a sync function and returns an async function that wraps it. Can be used
as either a direct wrapper or a decorator::

from asgiref.sync import sync_to_async

Expand All @@ -220,33 +234,32 @@ in its place. Can be used as either a direct wrapper or a decorator::
Threadlocals and contextvars values are preserved across the boundary in both
directions.

Synchronous functions tend to be written assuming they all run in the main
Sync functions tend to be written assuming they all run in the main
thread, so :func:`sync_to_async` has two threading modes:

* ``thread_sensitive=False`` (the default): the synchronous function will run
in a brand new thread which is then closed once it completes.
* ``thread_sensitive=False`` (the default): the sync function will run in a
brand new thread which is then closed once the invocation completes.

* ``thread_sensitive=True``: the synchronous function will run in the same
thread as all other ``thread_sensitive`` functions, and this will be the main
thread, if the main thread is synchronous and you are using the
:func:`async_to_sync` wrapper.
* ``thread_sensitive=True``: the sync function will run in the same thread as
all other ``thread_sensitive`` functions. This will be the main thread, if
the main thread is synchronous and you are using the :func:`async_to_sync`
wrapper.

Thread-sensitive mode is quite special, and does a lot of work to run all
functions in the same thread. Note, though, that it *relies on usage of*
:func:`async_to_sync` *above it in the stack* to correctly run things on the
main thread. If you use ``asyncio.run()`` (or other options instead), it will
fall back to just running thread-sensitive functions in a single, shared thread
(but not the main thread).
main thread. If you use ``asyncio.run()`` or similar, it will fall back to
running thread-sensitive functions in a single, shared thread, but this will
not be the main thread.

The reason this is needed in Django is that many libraries, specifically
database adapters, require that they are accessed in the same thread that they
were created in, and a lot of existing Django code assumes it all runs in the
same thread (e.g. middleware adding things to a request for later use by a
view).
were created in. Also a lot of existing Django code assumes it all runs in the
same thread, e.g. middleware adding things to a request for later use in views.

Rather than introduce potential compatibility issues with this code, we instead
opted to add this mode so that all existing Django synchronous code runs in the
same thread and thus is fully compatible with asynchronous mode. Note, that
synchronous code will always be in a *different* thread to any async code that
is calling it, so you should avoid passing raw database handles or other
thread-sensitive references around in any new code you write.
opted to add this mode so that all existing Django sync code runs in the same
thread and thus is fully compatible with async mode. Note that sync code will
always be in a *different* thread to any async code that is calling it, so you
should avoid passing raw database handles or other thread-sensitive references
around.
11 changes: 5 additions & 6 deletions docs/topics/http/middleware.txt
Original file line number Diff line number Diff line change
Expand Up @@ -308,10 +308,10 @@ on your middleware factory function or class:
asynchronous requests. Defaults to ``False``.

If your middleware has both ``sync_capable = True`` and
``async_capable = True``, then Django will pass it the request in whatever form
it is currently in. You can work out what type of request you have by seeing
if the ``get_response`` object you are passed is a coroutine function or not
(using :py:func:`asyncio.iscoroutinefunction`).
``async_capable = True``, then Django will pass it the request without
converting it. In this case, you can work out if your middleware will receive
async requests by checking if the ``get_response`` object you are passed is a
coroutine function, using :py:func:`asyncio.iscoroutinefunction`.

The ``django.utils.decorators`` module contains
:func:`~django.utils.decorators.sync_only_middleware`,
Expand All @@ -328,8 +328,7 @@ methods, if they are provided, should also be adapted to match the sync/async
mode. However, Django will individually adapt them as required if you do not,
at an additional performance penalty.

Here's an example of how to detect and adapt your middleware if it supports
both::
Here's an example of how to create a middleware function that supports both::

import asyncio
from django.utils.decorators import sync_and_async_middleware
Expand Down
19 changes: 10 additions & 9 deletions docs/topics/http/views.txt
Original file line number Diff line number Diff line change
Expand Up @@ -205,25 +205,26 @@ in a test view. For example::

.. _async-views:

Asynchronous views
==================
Async views
===========

.. versionadded:: 3.1

As well as being synchronous functions, views can also be asynchronous
functions (``async def``). Django will automatically detect these and run them
in an asynchronous context. You will need to be using an asynchronous (ASGI)
server to get the full power of them, however.
("async") functions, normally defined using Python's ``async def`` syntax.
Django will automatically detect these and run them in an async context.
However, you will need to use an async server based on ASGI to get their
performance benefits.

Here's an example of an asynchronous view::
Here's an example of an async view::

from django.http import HttpResponse
import datetime
from django.http import HttpResponse

async def current_datetime(request):
now = datetime.datetime.now()
html = '<html><body>It is now %s.</body></html>' % now
return HttpResponse(html)

You can read more about Django's asynchronous support, and how to best use
asynchronous views, in :doc:`/topics/async`.
You can read more about Django's async support, and how to best use async
views, in :doc:`/topics/async`.
2 changes: 1 addition & 1 deletion docs/topics/testing/tools.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1808,7 +1808,7 @@ creates.

@mock.patch(...)
@async_to_sync
def test_my_thing(self):
async def test_my_thing(self):
...

.. _topics-testing-email:
Expand Down

0 comments on commit 4216225

Please sign in to comment.