Skip to content

Commit

Permalink
[3.0.x] Refs #31224 -- Doc'd async adapter functions.
Browse files Browse the repository at this point in the history
Backport of 40a64dd from master
  • Loading branch information
andrewgodwin authored and felixxm committed Mar 13, 2020
1 parent d9f1792 commit 8d4638d
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 5 deletions.
2 changes: 1 addition & 1 deletion docs/ref/exceptions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ list of errors.

If you are trying to call code that is synchronous-only from an
asynchronous thread, then create a synchronous thread and call it in that.
You can accomplish this is with ``asgiref.sync.sync_to_async``.
You can accomplish this is with :func:`asgiref.sync.sync_to_async`.

.. currentmodule:: django.urls

Expand Down
3 changes: 3 additions & 0 deletions docs/spelling_wordlist
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ autogenerated
autoincrement
autoreload
autovacuum
awaitable
Azerbaijani
backend
backends
Expand Down Expand Up @@ -115,6 +116,7 @@ concat
conf
config
contenttypes
contextvars
contrib
coroutine
coroutines
Expand Down Expand Up @@ -666,6 +668,7 @@ th
that'll
Thejaswi
This'll
threadlocals
threadpool
timeframe
timeline
Expand Down
111 changes: 107 additions & 4 deletions docs/topics/async.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Asynchronous support

.. versionadded:: 3.0

.. currentmodule:: asgiref.sync

Django has developing support for asynchronous ("async") Python, but does not
yet support asynchronous views or middleware; they will be coming in a future
release.
Expand All @@ -15,7 +17,7 @@ safety support.
.. _async-safety:

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

Certain key parts of Django are not able to operate safely in an asynchronous
environment, as they have global state that is not coroutine-aware. These parts
Expand All @@ -28,13 +30,14 @@ 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 ``sync_to_async`` or a threadpool, then it
can also occur, as your code is still running in an asynchronous context.
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.

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
``asgiref.sync.async_to_sync``, or any other preferred way of running
:func:`asgiref.sync.async_to_sync`, 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
Expand All @@ -54,3 +57,103 @@ If you need to do this from within Python, do that with ``os.environ``::
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.

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
dependency when you install Django with ``pip``.

.. _asgiref: https://pypi.org/project/asgiref/

``async_to_sync()``
-------------------

.. 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::

from asgiref.sync import async_to_sync

sync_function = async_to_sync(async_function)

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

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.

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
as ensuring threadlocals work, it also enables the ``thread_sensitive`` mode of
:func:`sync_to_async` when that wrapper is used below it.

``sync_to_async()``
-------------------

.. 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::

from asgiref.sync import sync_to_async

async_function = sync_to_async(sync_function)
async_function = sync_to_async(sensitive_sync_function, thread_sensitive=True)

@sync_to_async
def sync_function(...):
...

@sync_to_async(thread_sensitive=True)
def sensitive_sync_function(...):
...

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
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=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 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).

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).

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.

0 comments on commit 8d4638d

Please sign in to comment.