Clarified async documentation.

This commit is contained in:
Adam Johnson 2020-03-26 15:46:24 +00:00 committed by GitHub
parent 6ef4c8aa9f
commit 4216225480
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 124 additions and 112 deletions

View File

@ -144,7 +144,6 @@ databrowse
datafile datafile
dataset dataset
datasets datasets
datastores
datatype datatype
datetimes datetimes
Debian Debian

View File

@ -8,15 +8,15 @@ Asynchronous support
Django has support for writing asynchronous ("async") views, along with an Django has support for writing asynchronous ("async") views, along with an
entirely async-enabled request stack if you are running under entirely async-enabled request stack if you are running under
:doc:`ASGI </howto/deployment/asgi/index>` rather than WSGI. Async views will :doc:`ASGI </howto/deployment/asgi/index>`. Async views will still work under
still work under WSGI, but with performance penalties, and without the ability WSGI, but with performance penalties, and without the ability to have efficient
to have efficient long-running requests. long-running requests.
We're still working on asynchronous support for the ORM and other parts of We're still working on async support for the ORM and other parts of Django.
Django; you can expect to see these in future releases. For now, you can use You can expect to see this in future releases. For now, you can use the
the :func:`sync_to_async` adapter to interact with normal Django, as well as :func:`sync_to_async` adapter to interact with the sync parts of Django.
use a whole range of Python asyncio libraries natively. See below for more There is also a whole range of async-native Python libraries that you can
details. integrate with.
.. versionchanged:: 3.1 .. versionchanged:: 3.1
@ -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 coroutine, ensure you set the ``_is_coroutine`` attribute of the view
to ``asyncio.coroutines._is_coroutine`` so this function returns ``True``. to ``asyncio.coroutines._is_coroutine`` so this function returns ``True``.
Under a WSGI server, asynchronous views will run in their own, one-off event Under a WSGI server, async views will run in their own, one-off event loop.
loop. This means that you can do things like parallel, async HTTP calls to APIs This means you can use async features, like parallel async HTTP requests,
without any issues, but you will not get the benefits of an asynchronous without any issues, but you will not get the benefits of an async stack.
request stack.
If you want these benefits - which are mostly around the ability to service The main benefits are the ability to service hundreds of connections without
hundreds of connections without using any Python threads (enabling slow using Python threads. This allows you to use slow streaming, long-polling, and
streaming, long-polling, and other exciting response types) - you will need to other exciting response types.
deploy Django using :doc:`ASGI </howto/deployment/asgi/index>` instead.
If you want to use these, you will need to deploy Django using
:doc:`ASGI </howto/deployment/asgi/index>` instead.
.. warning:: .. warning::
You will only get the benefits of a fully-asynchronous request stack if you 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 of synchronous middleware, then Django must use a thread per request to
safely emulate a synchronous environment for it. safely emulate a synchronous environment for it.
@ -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 on debug logging for the ``django.request`` logger and look for log
messages about *`"Synchronous middleware ... adapted"*. messages about *`"Synchronous middleware ... adapted"*.
In either ASGI or WSGI mode, though, you can safely use asynchronous support to In both ASGI and WSGI mode, you can still safely use asynchronous support to
run code in parallel rather than serially, which is especially handy when run code in parallel rather than serially. This is especially handy when
dealing with external APIs or datastores. dealing with external APIs or data stores.
If you want to call a part of Django that is still synchronous (like the ORM) 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:: you will need to wrap it in a :func:`sync_to_async` call. For example::
from asgiref.sync import sync_to_async 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 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 entire function using :func:`sync_to_async`. For example::
part of Django that is still synchronous-only from an async view, you will
trigger Django's :ref:`asynchronous safety protection <async-safety>` to from asgiref.sync import sync_to_async
protect your data from corruption.
@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 Performance
----------- -----------
@ -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 call style to allow your code to run. This context-switch causes a small
performance penalty of around a millisecond. performance penalty of around a millisecond.
This is true of middleware as well, however. Django will attempt to minimize This is also true of middleware. Django will attempt to minimize the number of
the number of context-switches. If you have an ASGI server, but all your context-switches between sync and async. If you have an ASGI server, but all
middleware and views are synchronous, it will switch just once, before it your middleware and views are synchronous, it will switch just once, before it
enters the middleware stack. 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 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 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, but bear open for middleware exception propagation. This may not be noticeable at first,
in mind that even adding a single piece of synchronous middleware can drag your but adding this penalty of one thread per request can remove any async
whole async project down to running with one thread per request, and the performance advantage.
associated performance penalties.
You should do your own performance testing to see what effect ASGI vs. WSGI has You should do your own performance testing to see what effect ASGI versus WSGI
on your code. In some cases, there may be a performance increase even for 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 a purely synchronous codebase under ASGI because the request-handling code is
still all running asynchronously. In general, though, you will only want to still all running asynchronously. In general you will only want to enable ASGI
enable ASGI mode if you have asynchronous code in your site. mode if you have asynchronous code in your project.
.. _async-safety: .. _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 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 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 an async environment. The ORM is the main example, but there are other parts
parts that are also protected in this way. that are also protected in this way.
If you try to run any of these parts from a thread where there is a *running If you try to run any of these parts from a thread where there is a *running
event loop*, you will get a event loop*, you will get a
:exc:`~django.core.exceptions.SynchronousOnlyOperation` error. Note that you :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 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 you have called a sync function directly from an async function,
without going through something like :func:`sync_to_async` or a threadpool, without using :func:`sync_to_async` or similar, then it can also occur. This is
then it can also occur, as your code is still running in an asynchronous because your code is still running in a thread with an active event loop, even
context. though it may not be declared as async code.
If you encounter this error, you should fix your code to not call the offending 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 code from an async context. Instead, write your code that talks to async-unsafe
in its own, synchronous function, and call that using functions in its own, sync function, and call that using
:func:`asgiref.sync.sync_to_async`, or any other preferred way of running :func:`asgiref.sync.sync_to_async` (or any other way of running sync code in
synchronous code in its own thread. its own thread).
If you are *absolutely* in dire need to run this code from an asynchronous You may still be forced to run sync code from an async context. For example,
context - for example, it is being forced on you by an external environment, if the requirement is forced on you by an external environment, such as in a
and you are sure there is no chance of it being run concurrently (e.g. you are Jupyter_ notebook. If you are sure there is no chance of the code being run
in a Jupyter_ notebook), then you can disable the warning with the concurrently, and you *absolutely* need to run this sync code from an async
``DJANGO_ALLOW_ASYNC_UNSAFE`` environment variable. context, then you can disable the warning by setting the
``DJANGO_ALLOW_ASYNC_UNSAFE`` environment variable to any value.
.. warning:: .. warning::
@ -147,6 +156,8 @@ 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``:: If you need to do this from within Python, do that with ``os.environ``::
import os
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
.. _Jupyter: https://jupyter.org/ .. _Jupyter: https://jupyter.org/
@ -154,11 +165,11 @@ If you need to do this from within Python, do that with ``os.environ``::
Async adapter functions Async adapter functions
======================= =======================
It is necessary to adapt the calling style when calling synchronous code from It is necessary to adapt the calling style when calling sync code from an async
an asynchronous context, or vice-versa. For this there are two adapter context, or vice-versa. For this there are two adapter functions, from the
functions, made available from the ``asgiref.sync`` package: ``asgiref.sync`` module: :func:`async_to_sync` and :func:`sync_to_async`. They
:func:`async_to_sync` and :func:`sync_to_async`. They are used to transition are used to transition between the calling styles while preserving
between sync and async calling styles while preserving compatibility. compatibility.
These adapter functions are widely used in Django. The `asgiref`_ package 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 itself is part of the Django project, and it is automatically installed as a
@ -171,28 +182,31 @@ dependency when you install Django with ``pip``.
.. function:: async_to_sync(async_function, force_new_loop=False) .. function:: async_to_sync(async_function, force_new_loop=False)
Wraps an asynchronous function and returns a synchronous function in its place. Takes an async function and returns a sync function that wraps it. Can be used
Can be used as either a direct wrapper or a decorator:: as either a direct wrapper or a decorator::
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
sync_function = async_to_sync(async_function) async def get_data(...):
@async_to_sync
async def async_function(...):
... ...
The asynchronous function is run in the event loop for the current thread, if sync_get_data = async_to_sync(get_data)
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 @async_to_sync
either situation, the async function will execute on a different thread to the async def get_other_data(...):
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 Threadlocals and contextvars values are preserved across the boundary in both
directions. directions.
:func:`async_to_sync` is essentially a more powerful version of the :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 as ensuring threadlocals work, it also enables the ``thread_sensitive`` mode of
:func:`sync_to_async` when that wrapper is used below it. :func:`sync_to_async` when that wrapper is used below it.
@ -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) .. function:: sync_to_async(sync_function, thread_sensitive=False)
Wraps a synchronous function and returns an asynchronous (awaitable) function Takes a sync function and returns an async function that wraps it. Can be used
in its place. Can be used as either a direct wrapper or a decorator:: as either a direct wrapper or a decorator::
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
@ -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 Threadlocals and contextvars values are preserved across the boundary in both
directions. 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, so :func:`sync_to_async` has two threading modes:
* ``thread_sensitive=False`` (the default): the synchronous function will run * ``thread_sensitive=False`` (the default): the sync function will run in a
in a brand new thread which is then closed once it completes. brand new thread which is then closed once the invocation completes.
* ``thread_sensitive=True``: the synchronous function will run in the same * ``thread_sensitive=True``: the sync function will run in the same thread as
thread as all other ``thread_sensitive`` functions, and this will be the main all other ``thread_sensitive`` functions. This will be the main thread, if
thread, if the main thread is synchronous and you are using the the main thread is synchronous and you are using the :func:`async_to_sync`
:func:`async_to_sync` wrapper. wrapper.
Thread-sensitive mode is quite special, and does a lot of work to run all 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* 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 :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 main thread. If you use ``asyncio.run()`` or similar, it will fall back to
fall back to just running thread-sensitive functions in a single, shared thread running thread-sensitive functions in a single, shared thread, but this will
(but not the main thread). not be the main thread.
The reason this is needed in Django is that many libraries, specifically 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 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 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 by a same thread, e.g. middleware adding things to a request for later use in views.
view).
Rather than introduce potential compatibility issues with this code, we instead 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 opted to add this mode so that all existing Django sync code runs in the same
same thread and thus is fully compatible with asynchronous mode. Note, that thread and thus is fully compatible with async mode. Note that sync code will
synchronous code will always be in a *different* thread to any async code that always be in a *different* thread to any async code that is calling it, so you
is calling it, so you should avoid passing raw database handles or other should avoid passing raw database handles or other thread-sensitive references
thread-sensitive references around in any new code you write. around.

View File

@ -308,10 +308,10 @@ on your middleware factory function or class:
asynchronous requests. Defaults to ``False``. asynchronous requests. Defaults to ``False``.
If your middleware has both ``sync_capable = True`` and If your middleware has both ``sync_capable = True`` and
``async_capable = True``, then Django will pass it the request in whatever form ``async_capable = True``, then Django will pass it the request without
it is currently in. You can work out what type of request you have by seeing converting it. In this case, you can work out if your middleware will receive
if the ``get_response`` object you are passed is a coroutine function or not async requests by checking if the ``get_response`` object you are passed is a
(using :py:func:`asyncio.iscoroutinefunction`). coroutine function, using :py:func:`asyncio.iscoroutinefunction`.
The ``django.utils.decorators`` module contains The ``django.utils.decorators`` module contains
:func:`~django.utils.decorators.sync_only_middleware`, :func:`~django.utils.decorators.sync_only_middleware`,
@ -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, mode. However, Django will individually adapt them as required if you do not,
at an additional performance penalty. at an additional performance penalty.
Here's an example of how to detect and adapt your middleware if it supports Here's an example of how to create a middleware function that supports both::
both::
import asyncio import asyncio
from django.utils.decorators import sync_and_async_middleware from django.utils.decorators import sync_and_async_middleware

View File

@ -205,25 +205,26 @@ in a test view. For example::
.. _async-views: .. _async-views:
Asynchronous views Async views
================== ===========
.. versionadded:: 3.1 .. versionadded:: 3.1
As well as being synchronous functions, views can also be asynchronous As well as being synchronous functions, views can also be asynchronous
functions (``async def``). Django will automatically detect these and run them ("async") functions, normally defined using Python's ``async def`` syntax.
in an asynchronous context. You will need to be using an asynchronous (ASGI) Django will automatically detect these and run them in an async context.
server to get the full power of them, however. 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 import datetime
from django.http import HttpResponse
async def current_datetime(request): async def current_datetime(request):
now = datetime.datetime.now() now = datetime.datetime.now()
html = '<html><body>It is now %s.</body></html>' % now html = '<html><body>It is now %s.</body></html>' % now
return HttpResponse(html) return HttpResponse(html)
You can read more about Django's asynchronous support, and how to best use You can read more about Django's async support, and how to best use async
asynchronous views, in :doc:`/topics/async`. views, in :doc:`/topics/async`.

View File

@ -1808,7 +1808,7 @@ creates.
@mock.patch(...) @mock.patch(...)
@async_to_sync @async_to_sync
def test_my_thing(self): async def test_my_thing(self):
... ...
.. _topics-testing-email: .. _topics-testing-email: