Browse Source

Refs #31224 -- Doc'd async adapter functions.

Andrew Godwin 5 years ago
parent
commit
40a64dd1e2
3 changed files with 111 additions and 5 deletions
  1. 1 1
      docs/ref/exceptions.txt
  2. 3 0
      docs/spelling_wordlist
  3. 107 4
      docs/topics/async.txt

+ 1 - 1
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
 

+ 3 - 0
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

+ 107 - 4
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.