异步视图

阅读: 23840     评论:4

纵观Django的发展史和版本提升过程。1.x时代持续了多年,直到1.11结束,其实也就是3年前的事。

再往后,Django的版本号就像开了挂似的,短短3年间大版本号进入了3.x时代。

但其实对普通开发人员而言,你能感觉到的变化,就是2.0后使用path代替正则来编写url,这也是从1.x迁移到2.x甚至3.x时,唯一需要重点关注的事情。

至于3.0,嗯嗯,引入了ASGI,但无法原生支持Websocket,只是给大家打点鸡血。

而3.1,支持异步视图,但不支持异步ORM,其实也没什么太多感觉。

异步视图

在Django中,可以通过async def语法,将任何函数视图定义为异步视图。

对于类视图,则是将它的__call__()方法定义为async def,成为异步视图。

Web服务器可分为WSGI和ASGI两种模式,在不同的模式下,同步和异步视图有性能差别:

  • WSGI+同步视图:传统模式
  • ASGI+异步视图:性能最佳的异步模式
  • WSGI+同步视图+异步视图:传统模式混杂异步,异步视图的性能不能正常发挥
  • ASGI+同步视图+异步视图:异步模式混杂传统视图,异步性能最佳

WSGI 服务器下,异步视图将在其自有的一次性事件循环中运行。这意味着你可以放心使用异步特性(例如并发异步 HTTP 请求),但是你不会获得异步堆栈的好处。

ASGI的主要优点则是无需使用 Python 线程就能服务数百个连接。这就允许你使用慢流(slow streaming)、长轮询和其他响应类型。

(附ASGI官方文档地址:https://asgi.readthedocs.io/en/latest/extensions.html)

说明:

如果你的站点中没有同步的中间件,那么你将得到完全的异步请求栈的好处。如果有一个同步中间件,那么Django必须在每个请求中使用一个线程来安全地为它模拟一个同步环境。

中间件可以同时支持同步和异步上下文。Django的一些内置中间件就是这么构建的,但不是所有都这样。换句话说,就是目前你还无法原生得到完全的异步请求栈的性能。

由于ORM对数据库的访问这块依然没有实现异步处理,还是同步的。所以在一个异步视图中,你需要使用Django给你的sync_to_async() 方法将ORM操作包裹起来。例如:

from asgiref.sync import sync_to_async

results = await sync_to_async(Blog.objects.get, thread_sensitive=True)(pk=123)
# 一定要注意get后面没有括号,因为是传递的ORM操作方法,而不是操作的结果

更好的做法是将ORM操作封装到一个函数中,然后再调用,如下所示:

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的异步安全保护模式(asynchronous safety protection)来保护你的数据不被破坏。

关于性能

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

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

但是,如果你把同步中间件放在ASGI服务器和异步视图之间,那么它必须先切换到中间件的同步模式然后再切换到视图的异步模式。Django还将为中间件异常传播(middleware exception propagation)保持同步线程的存在。起初可能并不明显,但为每个请求增加一个线程的代价会抵消任何异步带来的性能优势。

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

异步安全

Django 的一些关键部分不能在异步环境中安全运行,因为它们的全局状态不支持协同状态。这些部分被归类为"异步不安全",并且受到保护,不能在异步环境中执行,主要代表是ORM。

如果你试着从有运行事件循环的线程中运行这部分中的任何一个,你会得到一个 SynchronousOnlyOperation 错误。注意,不只是在异步函数内部才会出现这个错误。如果你从异步函数中调用一个同步函数,而没有使用 sync_to_async() 或类似方法,也会出现这个问题。

某些极端情况下,你可能被迫从异步上下文中运行同步代码。比如,如果需求是由外部环境强加给你的,比如在 Jupyter notebook 中。此时,可以通过设置 DJANGO_ALLOW_ASYNC_UNSAFE 环境变量来关掉这个警告,但一定要小心小心再小心。

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

import os

os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"

异步/同步适配函数

为了自适应同步和异步调用的调用,Django在asgiref.sync模块中提供了两个非常重要和关键的方法:

  • async_to_sync()
  • sync_to_async()

asgiref库是Django生态的一部分,由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(...):
    ...

async_to_sync() 本质上是 Python 标准库中 asyncio.run() 函数更强大的版本。

sync_to_async()

  • sync_to_async(sync_function, thread_sensitive=False)

同步转异步。

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

from asgiref.sync import sync_to_async

async_function = sync_to_async(sync_function)
async_function = sync_to_async(sensitive_sync_function, thread_sensitive=True)

@sync_to_async
def sync_function(...):
    ...

sync_to_async() 有两种线程模式:

  • thread_sensitive=False (默认):同步函数将在一个全新的线程中运行,该线程一旦完成,将会关闭。
  • thread_sensitive=True: 同步函数将与所有其它 thread_sensitive=True的 函数在相同线程里运行,这个线程通常就是主线程。

Thread-sensitive(线程敏感)模式非常特殊,在同一个线程中运行所有函数需要做很多工作。

ASGI服务器

如同WSGI一样,在使用startproject命名创建Django工程后,会自动生成一个ASGI应用配置文件,它就是<project_name>/asgi.py

通过它的asgi:application,你可以启动ASGI服务器。

如果你不想使用默认的这个配置文件,也可以自己编写,然后配置DJANGO_SETTINGS_MODULE,指向它。但一般人没有这个水平,还是用Django提供的更好。

我们不能用python manage.py runserver的方式启动ASGI服务器,这只会启动传统的、默认的WSGI服务器,也就是同步架构的服务器。我们需要安装专门的ASGI服务器。

类似WSGI下的uwsgi、gunicorn,ASGI下有三种可以使用的异步服务器:

  • Daphne
  • Hypercorn
  • Uvicorn

这里推荐使用Daphne,这是Django软件基金会开发的一个基于ASGI (HTTP/WebSocket)的服务器。

另外,如果你想插入ASGI中间件,或者将Django嵌入到其它ASGI应用中,你可以将Django的application对象包装到asgi.py文件中,如下所示:

from some_asgi_library import AmazingMiddleware
application = AmazingMiddleware(application)

Daphne

官网:https://pypi.org/project/daphne/

这里有详细的如何配置和启动Daphne的说明文档。主要学习如何指定ip和port。

最简单的方法就是:

  1. pip安装daphne
  2. 执行daphne your_project.asgi:application命令,启动ASGI服务器
  3. 浏览器访问127.0.0.1:8000,现在Django可以支持异步视图了

更多启动命令例子如下:

daphne -b 0.0.0.0 -p 8001 django_project.asgi:application

daphne -u /tmp/daphne.sock django_project.asgi:application

daphne --fd 5 django_project.asgi:application

daphne -e ssl:443:privateKey=key.pem:certKey=crt.pem django_project.asgi:application  

安装daphne的过程中,会同时安装很多依赖包,如下所示:

(venv) D:\work\for_test\django3>pip list
Package          Version
---------------- -------
asgiref          3.2.3
attrs            19.3.0
autobahn         19.11.1
Automat          0.8.0
cffi             1.13.2
constantly       15.1.0
cryptography     2.8
daphne           2.4.0
Django           3.0
hyperlink        19.0.0
idna             2.8
incremental      17.5.0
pip              10.0.1
pyasn1           0.4.8
pyasn1-modules   0.2.7
pycparser        2.19
PyHamcrest       1.9.0
pyOpenSSL        19.1.0
pytz             2019.3
service-identity 18.1.0
setuptools       39.1.0
six              1.13.0
sqlparse         0.3.0
Twisted          19.10.0
txaio            18.8.1
zope.interface   4.7.1

Django3.1 目前还不能原生支持Websocket。

更多关于ASGI服务器的内容,请阅读博客https://www.liujiangblog.com/blog/47/


 视图函数及快捷方式 HttpRequest对象 

评论总数: 4


点击登录后方可评论

是否意味着部署项目优先使用asgi服务器呢,即使都是同步视图,asgi也不差于wsgi?



异步写的很清晰明了。什么时候可以出一个专门写REST API 的专栏哦。



这玩意儿看的我一脸懵,异步视图是干嘛用的?用来做什么的?



对于大多数人,异步视图不是必须的,可以忽略。