异步支持Link to this heading

Django 支持编写异步(“async”)视图,如果在 ASGI 下运行,还支持完全异步的请求堆栈。异步视图仍然可以在 WSGI 下运行,但会有性能损失,并且不能有高效的长时间运行的请求。

我们仍然在为 ORM 和 Django 的其他部分提供异步支持。你可以期待在未来的版本中看到这个功能。目前,你可以使用 sync_to_async() 适配器来和 Django 的同步部分进行交互。你还可以集成一系列的原生异步 Python 库。

异步视图Link to this heading

任何视图可以通过使它的可调用部分返回一个协程来声明为异步——通常,这是使用 async def 完成的。对于基于函数的视图,需要使用 async def 来声明所有视图。对于基于类的视图,需要将它的 __call__() 方法作为 async def (而不是 __init__()as_view() )。

WSGI 服务器下,异步视图将在其自有的一次性事件循环中运行。这意味着你可以放心使用异步特性(例如并发异步 HTTP 请求),但是你不会获得异步堆栈的好处。

主要优点是无需使用 Python 线程就能服务数百个连接。这就允许你使用慢流(slow streaming)、长轮询和其他响应类型。

如果你想使用这些特性,需要使用 ASGI 来部署 Django。

在 ASGI 和 WSGI 模式里,你可以始终安全地使用异步支持来并发运行代码而不是串行。这在处理外部 API 或数据存储时特别方便。

如果你想调用仍处于同步的 Django 部分(比如 ORM),则需要用 sync_to_async() 调用来包装它。例如:

Code
from asgiref.sync import sync_to_async

results = await sync_to_async(Blog.objects.get, thread_sensitive=True)(pk=123)

你可能发现,移动任何 ORM 代码到它自己的函数中并使用 sync_to_async() 来调用整个函数会更容易。例如:

Code
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 的 异步安全保护 来保护你的数据不被破坏。

性能Link to this heading

在与视图不匹配的模式里运行时(比如在 WSGI 下的异步视图,在 ASGI 下的传统同步视图),Django 必须模拟其他调用方式来运行你的代码。这个上下文切换回导致大约 1 毫秒的小性能损失。

这对中间件也是如此。Django 将尝试最小化同步和异步之间上下文切换的次数。如果你有一个 ASGI 服务器,但所有中间件和视图是同步的,那么在进入中间件堆栈之前,它将仅切换一次。

但是,如果你把同步的中间件放在 ASGI 服务器和异步的视图之间,就必须为中间件切换到同步模式,然后再回到视图的异步模式。Django 还将保持同步线程的开放,以便中间件的异常传播。这可能在一开始并不明显,但增加每个请求一个线程的惩罚可以消除任何异步性能的优势。

你应该执行性能测试来观察 ASGI 和 WSGI 对你的代码有什么影响。在一些案例中,即使对于 ASGI 下的纯同步代码库,性能也可能会有所提高,因为请求处理代码仍然全部异步执行。通常,只有当项目有异步代码时,才需要开启 ASGI 模式。

异步安全Link to this heading

DJANGO_ALLOW_ASYNC_UNSAFELink to this definition

Django 的一些关键部分不能在异步环境中安全运行,因为它们的全局状态不支持协同状态。这些部分被归类为"异步不安全",并且受到保护,不能在异步环境中执行。ORM是主要的例子,但这里也有其他部分以这种方式受到保护。

如果你试着从有运行事件循环的线程中运行这部分中的任何一个,你会得到一个 SynchronousOnlyOperation 错误。注意,不用在异步函数内部就会得到这个错误。如果你从异步函数中调用一个同步函数,而没有使用 sync_to_async() 或类似方法,也会出现这个问题。这是因为你的代码仍然在具有活动事件循环的线程中运行,即使它可能没有被声明为异步代码。

如果遇到这个错误,你应该修改你的代码,以免从异步上下文中调用有问题的代码。相反,你可以编写代码在同步函数中与不安全异步交流,并使用 asgiref.sync.sync_to_async() 调用(或在自己的线程中运行同步代码的任何其他方式)。

你仍然可能被迫从异步上下文中运行同步代码。比如,如果需求是由外部环境强加给你的,比如在 Jupyter notebook 中。如果你确定不能同时运行代码,且 必须 要从异步上下文中运行这个同步代码 ,则可以通过设置 DJANGO_ALLOW_ASYNC_UNSAFE 环境变量为任何值来关掉这个警告。

如果你需要在 Python 中执行此操作,请使用 os.environ

Code
import os

os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"

异步适配函数Link to this heading

当从异步的上下文中调用同步的代码时,有必要适配调用风格,反之亦然。为此,有两个适配器功能,可从 asgiref.sync 模块中获取:async_to_sync()sync_to_async() 。它们用于调用样式之间转换,同时保持兼容性。

这些适配函数广泛应用于 Django。asgiref 包本身就是 Django 项目的部分,并且它在当你用 pip 方式安装 Django 时,会作为依赖项目自动安装。

async_to_sync()Link to this heading

async_to_sync(async_function, force_new_loop=False)Link to this definition

使用异步函数并返回包装它的同步函数。可用作直接包装器或装饰器:

Code
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()Link to this heading

sync_to_async(sync_function, thread_sensitive=True)Link to this definition

使用同步函数并返回包装它的异步函数。可用作直接包装器或装饰器:

Code
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 引用。