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.