异步支持 — Django 文档

来自菜鸟教程
Django/docs/3.2.x/topics/async
跳转至:导航、​搜索

异步支持

如果您在 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 同步代码在同一线程中运行,从而与异步模式完全兼容。 请注意,同步代码将始终位于与调用它的任何异步代码不同的 线程中,因此您应该避免传递原始数据库句柄或其他线程敏感的引用。