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
     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.
     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
 .. currentmodule:: django.urls
 
 

+ 3 - 0
docs/spelling_wordlist

@@ -44,6 +44,7 @@ autogenerated
 autoincrement
 autoincrement
 autoreload
 autoreload
 autovacuum
 autovacuum
+awaitable
 Azerbaijani
 Azerbaijani
 backend
 backend
 backends
 backends
@@ -115,6 +116,7 @@ concat
 conf
 conf
 config
 config
 contenttypes
 contenttypes
+contextvars
 contrib
 contrib
 coroutine
 coroutine
 coroutines
 coroutines
@@ -667,6 +669,7 @@ th
 that'll
 that'll
 Thejaswi
 Thejaswi
 This'll
 This'll
+threadlocals
 threadpool
 threadpool
 timeframe
 timeframe
 timeline
 timeline

+ 107 - 4
docs/topics/async.txt

@@ -4,6 +4,8 @@ Asynchronous support
 
 
 .. versionadded:: 3.0
 .. versionadded:: 3.0
 
 
+.. currentmodule:: asgiref.sync
+
 Django has developing support for asynchronous ("async") Python, but does not
 Django has developing support for asynchronous ("async") Python, but does not
 yet support asynchronous views or middleware; they will be coming in a future
 yet support asynchronous views or middleware; they will be coming in a future
 release.
 release.
@@ -15,7 +17,7 @@ safety support.
 .. _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 asynchronous
 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
@@ -28,13 +30,14 @@ 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 synchronous function directly from an asynchronous function
-without going through something like ``sync_to_async`` or a threadpool, then it
+without going through something like :func:`sync_to_async` or a threadpool,
-can also occur, as your code is still running in an asynchronous context.
+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
 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
 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.
 synchronous code in its own thread.
 
 
 If you are *absolutely* in dire need to run this code from an asynchronous
 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"
     os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
 
 
 .. _Jupyter: https://jupyter.org/
 .. _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.