异步支持

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

我们仍在为ORM和Django的其他部分提供异步支持。您可以在以后的版本中看到这一点。现在,您可以使用 sync_to_async() 适配器与Django的同步部分交互。您还可以集成一系列异步本机Python库。

异步视图

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

备注

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

在WSGI服务器下,异步视图将在它们自己的一次性事件循环中运行。这意味着您可以使用异步特性,例如并发异步HTTP请求,而不会出现任何问题,但是您将无法获得异步堆栈的好处。

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

如果您想使用这些,则需要使用 ASGI 相反。

警告

只有当您拥有完全异步的请求堆栈时,您才能从中获益 没有同步中间件 载入你的网站。如果有一个同步中间件,那么Django必须为每个请求使用一个线程来安全地为它模拟一个同步环境。

可以构建中间件来支持 both sync and async 上下文。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的 asynchronous safety protection 保护您的数据不受损坏。

装饰者

New in 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()

详细说明可在 异步查询 ,但简而言之:

  • QuerySet 导致发生SQL查询的方法具有 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下的传统sync视图),Django必须模拟其他调用样式,以允许代码运行。此上下文切换会导致大约一毫秒的性能损失。

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

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

你应该自己做性能测试,看看ASGI和WSGI对你的代码有什么影响。在某些情况下,甚至对于ASGI下的纯同步代码库,性能也可能会有所提高,因为请求处理代码仍然都是异步运行的。一般来说,如果项目中有异步代码,则只希望启用ASGI模式。

处理断开连接

New in Django 5.0.

对于长期请求,客户端可能会在视图返回响应之前断开连接。在本例中,一个 asyncio.CancelledError 将在视野中凸起。如果需要执行任何清理操作,您可以捕获并处理此错误::

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

你也可以 handle client disconnects in streaming responses

异步安全

DJANGO_ALLOW_ASYNC_UNSAFE

Django的某些关键部分不能在异步环境中安全地运行,因为它们的全局状态不是协同路由感知的。Django的这些部分被归类为“async-unsafe”,并且在异步环境中受到保护,不会被执行。ORM是主要的示例,但也有其他部分也以这种方式受到保护。

如果你试图从一个有 运行事件循环 ,您将获得 SynchronousOnlyOperation 错误。请注意,您不必直接在异步函数内部发生此错误。如果您直接从异步函数调用了sync函数,而不使用 sync_to_async() 或者类似的,那么它也可能发生。这是因为您的代码仍在具有活动事件循环的线程中运行,即使它可能未声明为异步代码。

如果遇到此错误,应修复代码,使其不从异步上下文调用有问题的代码。相反,编写与异步不安全函数进行对话的代码,即sync函数,并使用 asgiref.sync.sync_to_async() (或在自己的线程中运行同步代码的任何其他方式)。

运行Django代码的环境可能会强加给您异步上下文。例如, Jupyter 笔记本电脑和 IPython 交互式Shell都透明地提供了活动的事件循环,以便更容易地与异步API交互。

如果您使用的是IPythonShell程序,则可以通过运行以下命令来禁用此事件循环:

%autoawait off

作为IPython提示符下的命令。这将允许您运行同步代码,而无需生成 SynchronousOnlyOperation 错误;但是,您也不能 await 异步接口。要重新打开事件循环,请运行:

%autoawait on

如果您所在的环境不是IPython(或者您无法关闭 autoawait 在IPython中),您是 certain 您的代码不可能同时运行,而您 absolutely 需要从异步上下文运行同步代码,则可以通过设置 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项目的一部分,当您使用安装Django时,它会自动作为依赖项安装 pip

async_to_sync()

async_to_sync(async_function, force_new_loop=False)[源代码]

接受一个异步函数并返回一个封装它的sync函数。可以用作直接包装器或装饰器:

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() 本质上是一个更强大的版本 asyncio.run() 函数在Python的标准库中。除了确保threadlocal可以工作外,它还支持 thread_sensitive 模式 sync_to_async() 当包装纸在下面使用时。

sync_to_async()

sync_to_async(sync_function, thread_sensitive=True)[源代码]

获取一个sync函数并返回一个封装它的异步函数。可以用作直接包装器或装饰器:

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交互的值都是正确的,但是一定要评估 sync_to_async() 如果更新 asgiref 来自以前的版本。

Thread-sensitive mode is quite special, and does a lot of work to run all functions in the same thread. Note, though, that it relies on usage of async_to_sync() above it in the stack to correctly run things on the main thread. If you use asyncio.run() or similar, it will fall back to running thread-sensitive functions in a single, shared thread, but this will not be the main thread.

在Django中需要这样做的原因是许多库,特别是数据库适配器,都要求在创建它们的同一线程中访问它们。另外,许多现有的Django代码都假设它们都在同一个线程中运行,例如,中间件向请求中添加一些内容,以便以后在视图中使用。

我们没有引入与此代码潜在的兼容性问题,而是选择添加此模式,以便所有现有的Django sync代码在同一线程中运行,从而与异步模式完全兼容。请注意,同步代码将始终位于 不同的 线程指向调用它的任何异步代码,因此应避免传递原始数据库句柄或其他线程敏感引用。

实际上,这一限制意味着您不应该传递数据库的功能 connection 对象时调用 sync_to_async() 。这样做将触发线程安全检查:

# 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() 而不依赖于调用代码中的Connection对象。