异步支持¶
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 记录器的调试日志,并查找有关“Asynchronous handler adapted for middleware …”的日志消息。
无论是在 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 的 异步安全保护 以保护你的数据免受损坏。
装饰器¶
以下装饰器可同时用于同步和异步视图函数
conditional_page()xframe_options_deny()xframe_options_sameorigin()xframe_options_exempt()
例如:
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为前缀的异步变体。所有 QuerySet(包括
values()和values_list()的输出)均支持async for。
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() 调用它。
通过 CONN_MAX_AGE 设置的 持久数据库连接 也应在异步模式下禁用。如果可用,请改用数据库后端内置的连接池,或者根据需要调查第三方连接池选项。
性能¶
当运行模式与视图不匹配时(例如 WSGI 下的异步视图,或 ASGI 下的传统同步视图),Django 必须模拟另一种调用风格以允许你的代码运行。这种上下文切换会导致大约一毫秒的小额性能开销。
这也适用于中间件。Django 将尝试最小化同步和异步之间的上下文切换次数。如果你有 ASGI 服务器,但你所有的中间件和视图都是同步的,它只会切换一次,即在进入中间件堆栈之前。
但是,如果你将同步中间件放在 ASGI 服务器和异步视图之间,它将必须为中间件切换到同步模式,然后为视图切换回异步模式。Django 还将为中间件异常传播保持同步线程打开。这起初可能不明显,但每个请求增加一个线程的开销可能会抵消任何异步性能优势。
你应该进行自己的性能测试,以查看 ASGI 与 WSGI 对你的代码有什么影响。在某些情况下,即使是 ASGI 下纯同步的代码库也可能会提高性能,因为请求处理代码仍然全部异步运行。通常,只有在你的项目中包含异步代码时,才需要启用 ASGI 模式。
处理断开连接¶
对于长时间运行的请求,客户端可能在视图返回响应之前断开连接。在这种情况下,视图中会引发 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 Notebook 和 IPython 交互式 Shell 都透明地提供了一个活动的事件循环,以便更容易地与异步 API 进行交互。
如果你使用的是 IPython Shell,可以通过在 IPython 提示符下运行以下命令来禁用此事件循环
%autoawait off
这将允许你运行同步代码而不会产生 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 同步代码都在同一个线程中运行,从而完全兼容异步模式。注意,同步代码将始终与调用它的任何异步代码处于不同的线程中,因此你应该避免传递原始数据库句柄或其他线程敏感的引用。
实际上,这种限制意味着在调用 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() 调用,而无需依赖调用代码中的连接对象。