异步支持

Django 支持编写异步(“async”)视图,如果您在 ASGI 下运行,则可以使用完全启用异步的请求栈。异步视图仍然可以在 WSGI 下工作,但会带来性能损失,并且无法有效地处理长时间运行的请求。

我们仍在努力为 ORM 和 Django 的其他部分提供异步支持。您可以在将来的版本中看到这一点。目前,您可以使用 sync_to_async() 适配器与 Django 的同步部分交互。您还可以集成各种异步原生 Python 库。

异步视图

任何视图都可以通过使其可调用部分返回协程来声明为异步 - 通常,这是使用 async def 完成的。对于基于函数的视图,这意味着使用 async def 声明整个视图。对于基于类的视图,这意味着将 HTTP 方法处理程序(例如 get()post())声明为 async def(而不是其 __init__()as_view())。

注意

Django 使用 asgiref.sync.iscoroutinefunction 来测试您的视图是异步的还是同步的。如果您实现了您自己的返回协程的方法,请确保使用 asgiref.sync.markcoroutinefunction,以便此函数返回 True

在 WSGI 服务器下,异步视图将在其自己的单次事件循环中运行。这意味着您可以使用异步功能(如并发异步 HTTP 请求)而不会出现任何问题,但您不会获得异步栈的好处。

主要好处是能够在不使用 Python 线程的情况下服务数百个连接。这允许您使用缓慢的流、长轮询和其他令人兴奋的响应类型。

如果您想使用这些功能,则需要使用 ASGI 部署 Django。

警告

只有当您的站点中没有加载同步中间件时,您才能获得完全异步请求栈的好处。如果存在一段同步中间件,则 Django 必须为每个请求使用一个线程来安全地为其模拟同步环境。

可以构建中间件以支持 同步和异步 上下文。Django 的一些中间件就是这样构建的,但并非全部。要查看 Django 需要适应哪些中间件,您可以打开 django.request 记录器的调试日志,并查找有关“为中间件适配异步处理程序…”的日志消息。

在 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 的 异步安全保护 以保护您的数据免受损坏。

装饰器

Django 5.0 中的新功能。

以下装饰器可用于同步和异步视图函数

例如

from django.views.decorators.cache import never_cache


@never_cache
def my_sync_view(request): ...


@never_cache
async def my_async_view(request): ...

查询和 ORM

除了少数例外,Django 也可以异步运行 ORM 查询

async for author in Author.objects.filter(name__startswith="A"):
    book = await author.books.afirst()

详细信息可以在 异步查询 中找到,但简而言之

  • 所有导致 SQL 查询发生的 QuerySet 方法都有一个以 a 为前缀的异步变体。

  • async for 支持所有 QuerySet(包括 values()values_list() 的输出)。

Django 还支持一些使用数据库的异步模型方法

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() 调用它。

性能

在不匹配视图的模式下运行(例如,WSGI 下的异步视图或 ASGI 下的传统同步视图)时,Django 必须模拟其他调用样式以允许您的代码运行。此上下文切换会导致大约一毫秒的小性能损失。

中间件也是如此。Django 将尝试最大程度地减少同步和异步之间的上下文切换次数。如果您有 ASGI 服务器,但所有中间件和视图都是同步的,它将在进入中间件栈之前切换一次。

但是,如果您在 ASGI 服务器和异步视图之间放置同步中间件,则它必须切换到同步模式以用于中间件,然后切换回异步模式以用于视图。Django 还将保持同步线程打开以进行中间件异常传播。这可能一开始不会被注意到,但添加每个请求一个线程的这种惩罚可能会消除任何异步性能优势。

您应该进行自己的性能测试,以查看 ASGI 与 WSGI 对您的代码有什么影响。在某些情况下,即使在 ASGI 下的纯同步代码库中,也可能存在性能提升,因为请求处理代码仍然都在异步运行。一般来说,只有在项目中包含异步代码时,您才需要启用 ASGI 模式。

处理断开连接

Django 5.0 中的新功能。

对于长时间运行的请求,客户端可能在视图返回响应之前断开连接。在这种情况下,将在视图中引发 asyncio.CancelledError。如果需要执行任何清理,您可以捕获此错误并进行处理

async def my_view(request):
    try:
        # Do some work
        ...
    except asyncio.CancelledError:
        # Handle disconnect
        raise

您还可以 在流式响应中处理客户端断开连接

异步安全

DJANGO_ALLOW_ASYNC_UNSAFE

Django 的某些关键部分无法在异步环境中安全运行,因为它们具有不了解协程的全局状态。Django 的这些部分被归类为“异步不安全”,并且受到保护,无法在异步环境中执行。ORM 是主要示例,但还有其他部分也以这种方式受到保护。

如果您尝试在存在正在运行的事件循环的线程中运行这些部分中的任何一个,您将收到 SynchronousOnlyOperation 错误。请注意,您不必直接在异步函数内部才能发生此错误。如果您已从异步函数直接调用同步函数,而未使用 sync_to_async() 或类似方法,那么它也可能发生。这是因为您的代码仍在具有活动事件循环的线程中运行,即使它可能未声明为异步代码。

如果您遇到此错误,您应该修复您的代码,使其不要从异步上下文中调用有问题的代码。相反,将与异步不安全函数通信的代码写入其自己的同步函数中,并使用 asgiref.sync.sync_to_async()(或任何其他在自己的线程中运行同步代码的方法)调用它。

异步上下文可能会由您运行 Django 代码的环境强加于您。例如,Jupyter 笔记本和 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()包装器时启用了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 同步代码都在同一线程中运行,从而与异步模式完全兼容。请注意,同步代码始终位于与任何调用它的异步代码不同的线程中,因此您应该避免传递原始数据库句柄或其他线程敏感的引用。

在实践中,此限制意味着在调用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()调用,而无需依赖于调用代码中的连接对象。

返回顶部