异步支持¶
Django has support for writing asynchronous ("async") views, along with an entirely async-enabled request stack if you are running under ASGI. Async views will still work under WSGI, but with a small per-request adaptation cost (see 性能), and without the ability to have efficient long-running requests.
Many parts of Django provide asynchronous APIs, including the ORM, the cache framework, authentication, sessions, and signals.
For other code, the sync_to_async() adapter is a low-cost bridge (see
性能). A wide range of async-native Python libraries can
also be integrated.
异步视图¶
任何视图都可以通过使其可调用部分返回协程来声明为异步 - 通常情况下,可以使用 async def 来实现这一点。对于基于函数的视图,这意味着要使用 async def 来声明整个视图。对于基于类的视图,这意味着要将 HTTP 方法处理程序,如 get() 和 post() 声明为 async def (而不是 __init__() 或 as_view())。
Note
Django 使用 asgiref.sync.iscoroutinefunction 来测试你的视图是否是异步的。如果你实现了自己的协程返回方法,请确保使用 asgiref.sync.markcoroutinefunction,这样这个函数会返回 True。
在一个 WSGI 服务器下,异步视图将在它们自己的一次性事件循环中运行。这意味着你可以使用异步功能,比如并发的异步 HTTP 请求,而不会出现任何问题,但你不会得到异步堆栈的好处。
主要的好处是能够在不使用 Python 线程的情况下处理数百个连接。这使你能够使用慢速流式传输、长轮询和其他令人兴奋的响应类型。
如果你想使用这些特性,需要使用 ASGI 来部署 Django。
Note
A fully asynchronous request stack requires async middleware end-to-end. Where a piece of synchronous middleware sits between an ASGI server and an async view, Django adapts it by running it in its own thread; see 性能 for the cost trade-off.
Django's bundled middleware supports both sync and async. Third-party middleware may not. To see which
middleware Django adapts, turn on debug logging for the django.request
logger and look for log messages about "Asynchronous handler adapted for
middleware ...".
在 ASGI 和 WSGI 模式里,你可以始终安全地使用异步支持来并发运行代码而不是串行。这在处理外部 API 或数据存储时特别方便。
如果你想调用仍然是同步的 Django 部分,你需要将其包装在 sync_to_async() 调用中。例如:
from asgiref.sync import sync_to_async
results = await sync_to_async(sync_function, thread_sensitive=True)(pk=123)
如果你在异步视图中意外地尝试从 Django 中调用仅支持同步的部分,你将触发 Django 的 异步安全保护,以保护你的数据免受损坏。
装饰器¶
以下装饰器可以用于同步和异步视图函数:
conditional_page()xframe_options_deny()xframe_options_sameorigin()xframe_options_exempt()
例如:
from django.views.decorators.cache import never_cache
@never_cache
def my_sync_view(request): ...
@never_cache
async def my_async_view(request): ...
查询与 ORM¶
With some exceptions, Django can run ORM queries asynchronously:
async for author in Author.objects.filter(name__startswith="A"):
book = await author.books.afirst()
详细的说明可以在 异步查询 中找到,简而言之:
所有引发 SQL 查询的
QuerySet方法都有一个以a为前缀的异步变体。async for在所有的查询集上都得到支持(包括values()和values_list()的输出结果)。
Asynchronous model methods that use the database are also supported:
async def make_book(*args, **kwargs):
book = Book(...)
await book.asave(using="secondary")
async def make_book_with_tags(tags, *args, **kwargs):
book = await Book.objects.acreate(...)
await book.tags.aset(tags)
在异步模式下,事务还不可用。如果你有一段需要事务行为的代码,我们建议你将其编写为一个单独的同步函数,并使用 sync_to_async() 调用它。
Persistent database connections, set
via the CONN_MAX_AGE setting, should also be disabled in async mode.
Instead, use your database backend's built-in connection pooling if available,
or investigate a third-party connection pooling option if required. As in
synchronous Django, concurrent requests in a single process share that pool, so
size it to the target in-flight query concurrency.
性能¶
When running in a mode that does not match the view (e.g. an async view under WSGI, or a traditional sync view under ASGI), Django must emulate the other call style to allow your code to run. The per-call cost of this adaptation is small: tens of microseconds in the in-request ASGI path, where the running event loop is reused, and a few hundred microseconds in the cold-start path used by management commands, background tasks, and scripts. Against typical request times measured in milliseconds, this is rarely visible in itself, but can become so under GIL contention as the number of active threads grows.
If you find yourself wrapping individual rows or operations in a tight loop,
restructure your code so the loop runs inside a single sync_to_async()
(or async_to_sync()) crossing. The per-call cost of the context switch is
then spread across the whole loop and effectively disappears.
The same per-call adaptation cost applies to middleware. Django will attempt to minimize the number of context-switches between sync and async. If you have an ASGI server, but all your middleware and views are synchronous, it will switch just once, before it enters the middleware stack.
However, if you put synchronous middleware between an ASGI server and an asynchronous view, it will have to switch into sync mode for the middleware and then back to async mode for the view. Django will also hold the sync thread open for middleware exception propagation. For request/response views that hit the ORM and return, this is not usually a meaningful penalty. It matters most when you are using ASGI for high in-process concurrency over non-ORM I/O (for example upstream HTTP fan-out, server-sent events, or other long-lived requests), where the extra thread per request caps that concurrency.
你应该执行性能测试来观察 ASGI 和 WSGI 对你的代码有什么影响。在一些案例中,即使对于 ASGI 下的纯同步代码库,性能也可能会有所提高,因为请求处理代码仍然全部异步执行。通常,只有当项目有异步代码时,才需要开启 ASGI 模式。
处理断开连接¶
对于长时间运行的请求,在视图返回响应之前,客户端可能会断开连接。在这种情况下,视图会引发一个 asyncio.CancelledError。如果需要执行任何清理操作,你可以捕获这个错误并处理它:
async def my_view(request):
try:
# Do some work
...
except asyncio.CancelledError:
# Handle disconnect
raise
你还可以在流式响应中 处理客户端的断开连接。
异步安全¶
- DJANGO_ALLOW_ASYNC_UNSAFE¶
Certain key parts of Django are not able to operate safely in an async environment, as they have global state that is not coroutine-aware. These parts of Django are classified as "async-unsafe", and are protected from execution in an async environment. The synchronous API of the ORM is the main example, but there are other parts that are also protected in this way.
如果你试着从有运行事件循环的线程中运行这部分中的任何一个,你会得到一个 SynchronousOnlyOperation 错误。注意,不用在异步函数内部就会得到这个错误。如果你从异步函数中调用一个同步函数,而没有使用 sync_to_async() 或类似方法,也会出现这个问题。这是因为你的代码仍然在具有活动事件循环的线程中运行,即使它可能没有被声明为异步代码。
如果遇到这个错误,你应该修改你的代码,以免从异步上下文中调用有问题的代码。相反,你可以编写代码在同步函数中与不安全异步交流,并使用 asgiref.sync.sync_to_async() 调用(或在自己的线程中运行同步代码的任何其他方式)。
在运行你的 Django 代码环境中你可以使用异步上下文语境。例如, Jupyter 笔记本和 IPython 互动环境都是明显地提供了一种激活事件循环,所以与异步 APIs 互动更容易。
如果你正在使用 IPython shell,你可以通过运行以下命令来禁用这个事件循环:
%autoawait off
作为 IPython 提示符下的命令。这将允许你运行同步代码,而不会生成 SynchronousOnlyOperation 错误;但是,你也无法 await 异步 API。要重新启用事件循环,请运行:
%autoawait on
如果你在除了 IPython 之外的环境中(或者因某些原因无法在 IPython 中关闭 autoawait),并且你可以 确定 代码不会同时运行,而且你 绝对 需要从异步上下文中运行同步代码,那么您可以通过将 DJANGO_ALLOW_ASYNC_UNSAFE 环境变量设置为任何值来禁用警告。
Warning
如果启用此选项并且对 Django 的异步不安全部分进行并发访问,可能会导致数据丢失或损坏。请非常小心,不要在生产环境中使用此选项。
如果你需要在 Python 中执行此操作,请使用 os.environ :
import os
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
异步适配函数¶
当从异步的上下文中调用同步的代码时,有必要适配调用风格,反之亦然。为此,有两个适配器功能,可从 asgiref.sync 模块中获取:async_to_sync() 和 sync_to_async() 。它们用于调用样式之间转换,同时保持兼容性。
这些适配器函数在 Django 中被广泛使用。asgiref 包本身是 Django 项目的一部分,当你使用 pip 安装 Django 时,它会自动作为一个依赖项进行安装。
async_to_sync()¶
- async_to_sync(async_function, force_new_loop=False)¶
使用异步函数并返回包装它的同步函数。可用作直接包装器或装饰器:
from asgiref.sync import async_to_sync
async def get_data(): ...
sync_get_data = async_to_sync(get_data)
@async_to_sync
async def get_other_data(): ...
如果存在异步函数,那么它会在当前线程的事件循环中运行。如果没有当前事件循环,则会为单独异步调用专门启动一个新的事件循环,并且会在它完成后再次关闭。无论哪种情况,异步函数会在调用代码的不同线程上执行。
Threadlocals 和 contextvars 值在两个方向的边界上都保持不变。
async_to_sync() is essentially a more powerful version of the
asyncio.run() function in Python's standard library. As well as ensuring
threadlocals work, it also enables the thread_sensitive mode of
sync_to_async() when that wrapper is used below it. In the cold path
(no running event loop) it pays the cost of starting a fresh event loop, like
asyncio.run(); when an event loop is already running (the in-request ASGI
case), the running loop is reused and the cost drops accordingly.
sync_to_async()¶
- sync_to_async(sync_function, thread_sensitive=True)¶
使用同步函数并返回包装它的异步函数。可用作直接包装器或装饰器:
from asgiref.sync import sync_to_async
async_function = sync_to_async(sync_function, thread_sensitive=False)
async_function = sync_to_async(sensitive_sync_function, thread_sensitive=True)
@sync_to_async
def sync_function(): ...
Threadlocals 和 contextvars 值在两个方向的边界上都保持不变。
假设所有同步功能都在主线程中运行时,则倾向于编写同步功能,因此 sync_to_async() 有两个线程模式:
thread_sensitive=True(默认使用):同步函数将与所有其它thread_sensitive函数在相同线程里运行。如果主线程是同步的并且你正在使用async_to_sync()装饰器,则该同步函数将成为主线程。thread_sensitive=False:同步函数将在一个全新的线程中运行,该线程一旦完成,将会关闭。
Thread-sensitive(线程敏感)模式非常特殊,在同一个线程中运行所有函数需要做很多工作。但是请注意,它依赖于堆栈中它上面的 async_to_sync() 的使用,以便在主线程上正确运行。如果你使用 asyncio.run() 或类似,它将退回到单独共享线程(但不是主线程)中运行 thread-sensitive 函数。
在 Django 中需要这么做的原因是许多库,特别是数据库适配器,要求它们在创建时所在的同一个线程里对其进行访问。许多现有的 Django 代码也假设它都在同一进程中运行(比如中间件将内容添加到请求中以供稍后在视图中使用)。
我们没有引入代码潜在的兼容性问题,而是选择了添加这种模式,以便所有现有的 Django 同步代码都在同一个线程中运行,从而完全兼容异步模式。注意,同步代码始终要与调用它的异步代码保持在不同线程中,所以你应该避免传递原始数据库句柄(handles)或者其他 thread-sensitive 引用。
Within a single request, multiple thread_sensitive calls serialize on that
request's worker thread, but each request gets its own per-context worker, so
concurrent requests do not serialize against each other. This mirrors
Django's connection-per-thread model, and the same constraint applies in other
async database libraries, where concurrent queries on a single connection
serialize on a lock. To support more concurrent requests, increase the
connection pool size accordingly rather than disabling thread_sensitive.
在实际应用中,这意味着在调用 sync_to_async() 时,你不应该传递数据库 connection 对象的特性。这样做将触发线程安全检查:
# DJANGO_SETTINGS_MODULE=settings.py python -m asyncio
>>> import asyncio
>>> from asgiref.sync import sync_to_async
>>> from django.db import connection
>>> # In an async context so you cannot use the database directly:
>>> connection.cursor()
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from
an async context - use a thread or sync_to_async.
>>> # Nor can you pass resolved connection attributes across threads:
>>> await sync_to_async(connection.cursor)()
django.db.utils.DatabaseError: DatabaseWrapper objects created in a thread
can only be used in that same thread. The object with alias 'default' was
created in thread id 4371465600 and this is thread id 6131478528.
相反,您应该将所有数据库访问封装在一个帮助函数中,该函数可以使用 sync_to_async() 调用,而不依赖于调用代码中的连接对象。