异步支持

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() 调用中。例如:

  1. from asgiref.sync import sync_to_async
  2. results = await sync_to_async(sync_function, thread_sensitive=True)(pk=123)

如果你在异步视图中意外地尝试从 Django 中调用仅支持同步的部分,你将触发 Django 的 异步安全保护,以保护你的数据免受损坏。

装饰器

New in Django 5.0.

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

例如:

  1. from django.views.decorators.cache import never_cache
  2. @never_cache
  3. def my_sync_view(request): ...
  4. @never_cache
  5. async def my_async_view(request): ...

查询与 ORM

除了一些例外情况,Django 也可以异步运行 ORM 查询:

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

详细的说明可以在 异步查询 中找到,简而言之:

  • 所有引发 SQL 查询的 QuerySet 方法都有一个以 a 为前缀的异步变体。
  • async for 在所有的查询集上都得到支持(包括 values()values_list() 的输出结果)。

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

  1. async def make_book(*args, **kwargs):
  2. book = Book(...)
  3. await book.asave(using="secondary")
  4. async def make_book_with_tags(tags, *args, **kwargs):
  5. book = await Book.objects.acreate(...)
  6. await book.tags.aset(tags)

在异步模式下,事务还不可用。如果你有一段需要事务行为的代码,我们建议你将其编写为一个单独的同步函数,并使用 sync_to_async() 调用它。

性能

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

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

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

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

处理断开连接

New in Django 5.0.

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

  1. async def my_view(request):
  2. try:
  3. # Do some work
  4. ...
  5. except asyncio.CancelledError:
  6. # Handle disconnect
  7. raise

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

异步安全

DJANGO_ALLOW_ASYNC_UNSAFE

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

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

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

在运行你的 Django 代码环境中你可以使用异步上下文语境。例如, Jupyter 笔记本和 IPython 互动环境都是明显地提供了一种激活事件循环,所以与异步 APIs 互动更容易。

如果你正在使用 IPython shell,你可以通过运行以下命令来禁用这个事件循环:

  1. %autoawait off

作为 IPython 提示符下的命令。这将允许你运行同步代码,而不会生成 SynchronousOnlyOperation 错误;但是,你也无法 await 异步 API。要重新启用事件循环,请运行:

  1. %autoawait on

如果你在除了 IPython 之外的环境中(或者因某些原因无法在 IPython 中关闭 autoawait),并且你可以 确定 代码不会同时运行,而且你 绝对 需要从异步上下文中运行同步代码,那么您可以通过将 DJANGO_ALLOW_ASYNC_UNSAFE 环境变量设置为任何值来禁用警告。

警告

如果启用此选项并且对 Django 的异步不安全部分进行并发访问,可能会导致数据丢失或损坏。请非常小心,不要在生产环境中使用此选项。

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

  1. import os
  2. 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)

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

  1. from asgiref.sync import async_to_sync
  2. async def get_data(): ...
  3. sync_get_data = async_to_sync(get_data)
  4. @async_to_sync
  5. 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)

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

  1. from asgiref.sync import sync_to_async
  2. async_function = sync_to_async(sync_function, thread_sensitive=False)
  3. async_function = sync_to_async(sensitive_sync_function, thread_sensitive=True)
  4. @sync_to_async
  5. 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() 的使用情况。

Thread-sensitive(线程敏感)模式非常特殊,在同一个线程中运行所有函数需要做很多工作。但是请注意,它依赖于堆栈中它上面的 async_to_sync() 的使用,以便在主线程上正确运行。如果你使用 asyncio.run() 或类似,它将退回到单独共享线程(但不是主线程)中运行 thread-sensitive 函数。

在 Django 中需要这么做的原因是许多库,特别是数据库适配器,要求它们在创建时所在的同一个线程里对其进行访问。许多现有的 Django 代码也假设它都在同一进程中运行(比如中间件将内容添加到请求中以供稍后在视图中使用)。

我们没有引入代码潜在的兼容性问题,而是选择了添加这种模式,以便所有现有的 Django 同步代码都在同一个线程中运行,从而完全兼容异步模式。注意,同步代码始终要与调用它的异步代码保持在不同线程中,所以你应该避免传递原始数据库句柄(handles)或者其他 thread-sensitive 引用。

在实际应用中,这意味着在调用 sync_to_async() 时,你不应该传递数据库 connection 对象的特性。这样做将触发线程安全检查:

  1. # DJANGO_SETTINGS_MODULE=settings.py python -m asyncio
  2. >>> import asyncio
  3. >>> from asgiref.sync import sync_to_async
  4. >>> from django.db import connection
  5. >>> # In an async context so you cannot use the database directly:
  6. >>> connection.cursor()
  7. django.core.exceptions.SynchronousOnlyOperation: You cannot call this from
  8. an async context - use a thread or sync_to_async.
  9. >>> # Nor can you pass resolved connection attributes across threads:
  10. >>> await sync_to_async(connection.cursor)()
  11. django.db.utils.DatabaseError: DatabaseWrapper objects created in a thread
  12. can only be used in that same thread. The object with alias 'default' was
  13. created in thread id 4371465600 and this is thread id 6131478528.

相反,您应该将所有数据库访问封装在一个帮助函数中,该函数可以使用 sync_to_async() 调用,而不依赖于调用代码中的连接对象。