summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Godwin <andrew@aeracode.org>2020-03-10 10:01:19 -0600
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-03-13 07:13:44 +0100
commit40a64dd1e24d45f8e00a55b22a5174b8f1359b5c (patch)
treeaa4e7f8a7b55bf00a033e4bdcafb42b4250be75d
parent8725d04764e3528cbaefd0925501f8403d22ae3e (diff)
Refs #31224 -- Doc'd async adapter functions.
-rw-r--r--docs/ref/exceptions.txt2
-rw-r--r--docs/spelling_wordlist3
-rw-r--r--docs/topics/async.txt111
3 files changed, 111 insertions, 5 deletions
diff --git a/docs/ref/exceptions.txt b/docs/ref/exceptions.txt
index 34fec861e1..6b8d6707e4 100644
--- a/docs/ref/exceptions.txt
+++ b/docs/ref/exceptions.txt
@@ -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
diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist
index 1c651759f1..99bdba17b3 100644
--- a/docs/spelling_wordlist
+++ b/docs/spelling_wordlist
@@ -44,6 +44,7 @@ autogenerated
autoincrement
autoreload
autovacuum
+awaitable
Azerbaijani
backend
backends
@@ -115,6 +116,7 @@ concat
conf
config
contenttypes
+contextvars
contrib
coroutine
coroutines
@@ -667,6 +669,7 @@ th
that'll
Thejaswi
This'll
+threadlocals
threadpool
timeframe
timeline
diff --git a/docs/topics/async.txt b/docs/topics/async.txt
index b341084b1a..c6fa119984 100644
--- a/docs/topics/async.txt
+++ b/docs/topics/async.txt
@@ -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.
@@ -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
@@ -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
@@ -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.