Django 支持编写异步(“async”)视图,如果在 ASGI 下运行,还支持完全异步的请求堆栈。异步视图仍然可以在 WSGI 下运行,但会有性能损失,并且不能有高效的长时间运行的请求。
我们仍然在为 ORM 和 Django 的其他部分提供异步支持。你可以期待在未来的版本中看到这个功能。目前,你可以使用 sync_to_async()
适配器来和 Django 的同步部分进行交互。你还可以集成一系列的原生异步 Python 库。
已添加对异步视图的支持。
任何视图可以通过使它的可调用部分返回一个协程来声明为异步——通常,这是使用 async def
完成的。对于基于函数的视图,需要使用 async def
来声明所有视图。对于基于类的视图,需要将它的 __call__()
方法作为 async def
(而不是 __init__()
或 as_view()
)。
注解
Django 使用 asyncio.iscoroutinefunction
来测试视图是否为异步。如果你实现了自己的方法来返回协同程序,请确保你把视图的 _is_coroutine
属性设置为 asyncio.coroutines._is_coroutine
,这样函数将返回 True
。
WSGI 服务器下,异步视图将在其自有的一次性事件循环中运行。这意味着你可以放心使用异步特性(例如并发异步 HTTP 请求),但是你不会获得异步堆栈的好处。
主要优点是无需使用 Python 线程就能服务数百个连接。这就允许你使用慢流(slow streaming)、长轮询和其他响应类型。
如果你想使用这些特性,需要使用 ASGI 来部署 Django。
警告
如果你的站点中没有 非同步中间件,那么你将得到完全异步请求栈的好处。如果有一个同步中间件,那么 Django 必须在每个请求中使用一个线程来安全地为它模拟一个同步环境。
可以构建中间件来支持 同步和异步 上下文。一些 Django 中间件是这么构建的,但不是所有都这样。要查看 Django 能够支持哪些中间件,你可以为 django.request
记录器打开调试日志,而且要查看有关 "Synchronous middleware ... adapted" 的日志消息。
在 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 必须模拟其他调用方式来运行你的代码。这个上下文切换回导致大约 1 毫秒的小性能损失。
这对中间件也是如此。Django 将尝试最小化同步和异步之间上下文切换的次数。如果你有一个 ASGI 服务器,但所有中间件和视图是同步的,那么在进入中间件堆栈之前,它将仅切换一次。
但是,如果你把同步的中间件放在 ASGI 服务器和异步的视图之间,就必须为中间件切换到同步模式,然后再回到视图的异步模式。Django 还将保持同步线程的开放,以便中间件的异常传播。这可能在一开始并不明显,但增加每个请求一个线程的惩罚可以消除任何异步性能的优势。
你应该执行性能测试来观察 ASGI 和 WSGI 对你的代码有什么影响。在一些案例中,即使对于 ASGI 下的纯同步代码库,性能也可能会有所提高,因为请求处理代码仍然全部异步执行。通常,只有当项目有异步代码时,才需要开启 ASGI 模式。
DJANGO_ALLOW_ASYNC_UNSAFE
¶Django 的一些关键部分不能在异步环境中安全运行,因为它们的全局状态不支持协同状态。这些部分被归类为"异步不安全",并且受到保护,不能在异步环境中执行。ORM是主要的例子,但这里也有其他部分以这种方式受到保护。
如果你试着从有运行事件循环的线程中运行这部分中的任何一个,你会得到一个 SynchronousOnlyOperation
错误。注意,不用在异步函数内部就会得到这个错误。如果你从异步函数中调用一个同步函数,而没有使用 sync_to_async()
或类似方法,也会出现这个问题。这是因为你的代码仍然在具有活动事件循环的线程中运行,即使它可能没有被声明为异步代码。
如果遇到这个错误,你应该修改你的代码,以免从异步上下文中调用有问题的代码。相反,你可以编写代码在同步函数中与不安全异步交流,并使用 asgiref.sync.sync_to_async()
调用(或在自己的线程中运行同步代码的任何其他方式)。
在运行你的 Django 代码环境中你可以使用异步上下文语境。例如, Jupyter 笔记本和 IPython 互动环境都是明显地提供了一种激活事件循环,所以与异步 APIs 互动更容易。
如果你正在使用 IPython 环境,你可以通过运行如下命令来禁用这种事件循环:
%autoawait off
作为IPython终端的命令。这会使你得以运行同步代码而不产生:exc:~django.core.exceptions.SynchronousOnlyOperation 错误。然而,你无法等候异步API。重新打开事件循环,请运行:
%autoawait on
If you're in an environment other than IPython (or you can't turn off
autoawait
in IPython for some reason), you are certain there is no chance
of your code being run concurrently, and you absolutely need to run your sync
code from an async context, then you can disable the warning by setting the
DJANGO_ALLOW_ASYNC_UNSAFE
environment variable to any value.
警告
如果启用了这个选项,并且同时访问 Django 的异步不安全 (async-unsafe) 部分,你会遇到数据丢失或损坏,所以一定要非常小心,并且不要再生产环境里这样使用。
如果你需要在 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()
的使用情况。
Thread-sensitive(线程敏感)模式非常特殊,在同一个线程中运行所有函数需要做很多工作。但是请注意,它依赖于堆栈中它上面的 async_to_sync()
的使用,以便在主线程上正确运行。如果你使用 asyncio.run()
或类似,它将退回到单独共享线程(但不是主线程)中运行 thread-sensitive 函数。
在 Django 中需要这么做的原因是许多库,特别是数据库适配器,要求它们在创建时所在的同一个线程里对其进行访问。许多现有的 Django 代码也假设它都在同一进程中运行(比如中间件将内容添加到请求中以供稍后在视图中使用)。
我们没有引入代码潜在的兼容性问题,而是选择了添加这种模式,以便所有现有的 Django 同步代码都在同一个线程中运行,从而完全兼容异步模式。注意,同步代码始终要与调用它的异步代码保持在不同线程中,所以你应该避免传递原始数据库句柄(handles)或者其他 thread-sensitive 引用。
12月 07, 2021