异步支持 — Django 文档
异步支持
如果您在 ASGI 下运行,Django 支持编写异步(“async”)视图,以及完全启用异步的请求堆栈。 异步视图仍然可以在 WSGI 下工作,但会降低性能,并且无法有效地长时间运行请求。
我们仍在为 ORM 和 Django 的其他部分提供异步支持。 您可以期待在未来的版本中看到这一点。 现在,您可以使用 sync_to_async() 适配器与 Django 的同步部分进行交互。 您还可以集成一系列异步本机 Python 库。
3.1 版更改: 添加了对异步视图的支持。
异步视图
3.1 版中的新功能。
任何视图都可以通过使它的可调用部分返回一个协程来声明异步 - 通常,这是使用 async def
完成的。 对于基于函数的视图,这意味着使用 async def
声明整个视图。 对于基于类的视图,这意味着将其 __call__()
方法设为 async def
(而不是其 __init__()
或 as_view()
)。
笔记
Django 使用 asyncio.iscoroutinefunction
来测试你的视图是否是异步的。 如果您实现自己的返回协程的方法,请确保将视图的 _is_coroutine
属性设置为 asyncio.coroutines._is_coroutine
,以便此函数返回 True
。
在 WSGI 服务器下,异步视图将在它们自己的一次性事件循环中运行。 这意味着您可以使用异步功能,如并发异步 HTTP 请求,没有任何问题,但您不会获得异步堆栈的好处。
主要好处是能够在不使用 Python 线程的情况下为数百个连接提供服务。 这允许您使用慢流、长轮询和其他令人兴奋的响应类型。
如果你想使用这些,你需要使用 ASGI 来部署 Django。
警告
如果您的站点中没有加载 同步中间件 ,那么您只会获得完全异步请求堆栈的好处。 如果有一个同步中间件,那么 Django 必须为每个请求使用一个线程来安全地为其模拟同步环境。
可以构建中间件以支持 [X35X] 同步和异步 上下文。 Django 的一些中间件是这样构建的,但不是全部。 要查看 Django 必须适应哪些中间件,您可以打开 django.request
记录器的调试日志记录并查找有关 “同步中间件……已适应” 的日志消息。
在 ASGI 和 WSGI 模式下,您仍然可以安全地使用异步支持来并发而不是串行运行代码。 这在处理外部 API 或数据存储时特别方便。
如果你想调用仍然同步的 Django 部分,比如 ORM,你需要将它包装在 sync_to_async() 调用中。 例如:
from asgiref.sync import sync_to_async
results = await sync_to_async(Blog.objects.get, thread_sensitive=True)(pk=123)
您可能会发现将任何 ORM 代码移动到其自己的函数中并使用 sync_to_async() 调用整个函数更容易。 例如:
from asgiref.sync import sync_to_async
def _get_blog(pk):
return Blog.objects.select_related('author').get(pk=pk)
get_blog = sync_to_async(_get_blog, thread_sensitive=True)
如果您不小心尝试从异步视图调用仍然是同步的 Django 部分,您将触发 Django 的 异步安全保护 以保护您的数据免受损坏。
表现
在与视图不匹配的模式下运行时(例如 WSGI 下的异步视图,或 ASGI 下的传统同步视图),Django 必须模拟其他调用样式以允许您的代码运行。 这种上下文切换会导致大约一毫秒的小性能损失。
中间件也是如此。 Django 将尝试最小化同步和异步之间的上下文切换次数。 如果你有一个 ASGI 服务器,但是你所有的中间件和视图都是同步的,它在进入中间件堆栈之前只会切换一次。
但是,如果您在 ASGI 服务器和异步视图之间放置同步中间件,则它必须为中间件切换到同步模式,然后为视图切换回异步模式。 Django 还将为中间件异常传播保持同步线程打开。 一开始这可能并不明显,但是每个请求增加一个线程的惩罚可以消除任何异步性能优势。
您应该进行自己的性能测试,看看 ASGI 与 WSGI 对您的代码有何影响。 在某些情况下,即使是 ASGI 下的纯同步代码库也可能会提高性能,因为请求处理代码仍然全部异步运行。 一般来说,如果您的项目中有异步代码,您只需要启用 ASGI 模式。
异步安全
- DJANGO_ALLOW_ASYNC_UNSAFE
Django 的某些关键部分无法在异步环境中安全运行,因为它们具有无法感知协程的全局状态。 Django 的这些部分被归类为“async-unsafe”,并且在异步环境中不会被执行。 ORM 是主要示例,但还有其他部分也以这种方式受到保护。
如果您尝试从存在 运行事件循环 的线程中运行这些部分中的任何一个,您将收到 SynchronousOnlyOperation 错误。 请注意,您不必直接在异步函数内即可发生此错误。 如果您直接从异步函数调用了同步函数,而不使用 sync_to_async() 或类似方法,那么它也可能发生。 这是因为您的代码仍在具有活动事件循环的线程中运行,即使它可能未声明为异步代码。
如果您遇到此错误,您应该修复您的代码,使其不从异步上下文调用有问题的代码。 相反,编写与异步不安全函数通信的代码,同步函数,并使用 asgiref.sync.sync_to_async()(或在其自己的线程中运行同步代码的任何其他方式)调用它.
异步上下文可以由您运行 Django 代码的环境强加给您。 例如,Jupyter notebooks 和 IPython 交互式 shell 都透明地提供了一个活动事件循环,以便更容易与异步 API 交互。
如果您使用的是 IPython shell,则可以通过运行以下命令禁用此事件循环:
%autoawait off
作为 IPython 提示符下的命令。 这将允许您运行同步代码而不会产生 SynchronousOnlyOperation 错误; 但是,您也无法 await
异步 API。 要重新打开事件循环,请运行:
%autoawait on
如果您在 IPython 以外的环境中(或者由于某种原因您无法在 IPython 中关闭 autoawait
),那么您 确定 没有机会同时运行您的代码,并且您 绝对 需要从异步上下文运行同步代码,然后您可以通过将 DJANGO_ALLOW_ASYNC_UNSAFE 环境变量设置为任何值来禁用警告。
警告
如果您启用此选项并且并发访问 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() 本质上是 Python 标准库中 asyncio.run()
函数的更强大版本。 除了确保 threadlocals 工作外,它还启用 sync_to_async() 的 thread_sensitive
模式,当该包装器在其下方使用时。
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
:同步函数将在一个全新的线程中运行,一旦调用完成,该线程就会关闭。
警告
asgiref
3.3.0版本将thread_sensitive
参数的默认值改为True
。 这是一个更安全的默认值,并且在许多情况下与 Django 交互正确的值,但如果从以前的版本更新 asgiref
,请务必评估 sync_to_async()
的使用。
线程敏感模式非常特殊,在同一个线程中运行所有功能需要做很多工作。 但请注意,它 依赖于 async_to_sync() 在堆栈 上方的使用,才能在主线程上正确运行。 如果您使用 asyncio.run()
或类似的,它将回退到在单个共享线程中运行线程敏感函数,但这不会是主线程。
在 Django 中需要这样做的原因是许多库,特别是数据库适配器,要求在创建它们的同一线程中访问它们。 此外,许多现有的 Django 代码都假设它们都在同一个线程中运行,例如 中间件向请求添加内容以供以后在视图中使用。
我们没有引入此代码的潜在兼容性问题,而是选择添加此模式,以便所有现有的 Django 同步代码在同一线程中运行,从而与异步模式完全兼容。 请注意,同步代码将始终位于与调用它的任何异步代码不同的 线程中,因此您应该避免传递原始数据库句柄或其他线程敏感的引用。