当前位置:首页>python>Python 并发与 asyncio 9.4 Django 的异步视图

Python 并发与 asyncio 9.4 Django 的异步视图

  • 2026-10-17 19:25:19
Python 并发与 asyncio 9.4 Django 的异步视图

9.4 Django 的异步视图

作为最流行、使用最广泛的 Python 框架之一,Django 出厂即带着一堆实用功能:强悍的 对象关系映射器(ORM)帮你操控数据库,自带的可定制后台管理系统让你效率飞升。

过去,直到版本 3.0 以前,Django 仅支持以 WSGI 方式部署,几乎不支持异步。主要依靠第三方的 channels 库来满足部分需求。但从 3.0 版本开始,它正式引入了对 ASGI 支持,开启了向全异步演进的大门。到了 3.1 版本,更是实现了异步视图,让开发者能直接在视图中使用 asyncio 语言。

当然,这还处于早期阶段。比如,它的数据库引擎仍保持同步(但未来的计划是支持异步),整体能力还在不断完善。不过别担心,随着 Django 不断进化,这块短板会逐渐补齐。

我们来实战一下:用 Django 构建一个视图,去调用 aiohttp 进行并发请求。设想你正在集成一个外部 API,需要跑一批并发请求,查看每个请求的响应时间、返回体长度,还有失败次数。我们希望做一个页面,接收一个目标网址和请求数,然后并发发起请求,最后把结果以表格形式返回。

先确认你的 Django 版本:

pip install -Iv django==3.2.8

创建项目:

django-admin startproject async_views

生成文件夹结构:

async_views/    manage.py    async_views/        __init__.py        settings.py        urls.py        asgi.py        wsgi.py

注意:既有 wsgi.py 又有 asgi.py,这说明它既能在 WSGI 下运行,也能在 ASGI 下运行,超灵活!

现在可以用 Uvicorn 试试默认的 Django 欢迎页:

gunicorn async_views.asgi:application -k uvicorn.workers.UvicornWorker

打开 http://localhost:8000,你应该就能看到经典的 Django 欢迎页。

接着,我们来创建一个叫 async_api 的应用:

python manage.py startapp async_api

现在,万事俱备。我们来写一个异步视图。

列表 9.11 一个 Django 异步视图
import asynciofrom datetime import datetimefrom aiohttp import ClientSessionfrom django.shortcuts import renderimport aiohttpasync def get_url_details(session: ClientSession, url: str):    start_time = datetime.now()    response = await session.get(url)    response_body = await response.text()    end_time = datetime.now()    return {'status': response.status,            'time': (end_time - start_time).microseconds,            'body_length': len(response_body)}async def make_requests(url: str, request_num: int):    async with aiohttp.ClientSession() as session:        requests = [get_url_details(session, url) for _ in range(request_num)]        results = await asyncio.gather(*requests, return_exceptions=True)        failed_results = [str(result) for result in results if isinstance(result, Exception)]        successful_results = [result for result in results if not isinstance(result, Exception)]        return {'failed_results': failed_results, 'successful_results': successful_results}async def requests_view(request):    url: str = request.GET['url']    request_num: int = int(request.GET['request_num'])    context = await make_requests(url, request_num)    return render(request, 'async_api/requests.html', context)

这个视图做了两件事:第一,封装了 get_url_details 协程,发送请求并收集状态、时间和响应体长度。第二,定义了一个 requests_view 异步视图,它从查询参数中拿到 url 和 request_num,用 gather 并发执行所有请求,把成功和失败的结果分类,最后交给 render 渲染成页面。

但是我们还没建模板!现在先把视图模板建好。

先创建目录结构:

async_api/    templates/        async_api/            requests.html

然后写 templates/async_api/requests.html:

列表 9.12 requests 视图模板
<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <title>Request Summary</title></head><body><h1>Summary of requests:</h1><h2>Failures:</h2><table>    {% for failure in failed_results %}    <tr>        <td>{{failure}}</td>    </tr>    {% endfor %}</table><h2>Successful Results:</h2><table>    <tr>        <td>Status code</td>        <td>Response time (microseconds)</td>        <td>Response size</td>    </tr>    {% for result in successful_results %}    <tr>        <td>{{result.status}}</td>        <td>{{result.time}}</td>        <td>{{result.body_length}}</td>    </tr>    {% endfor %}</table></body></html>

这个页面会列出所有异常和成功结果,虽然看着不太美,但信息绝对全。

接下来,我们要把视图和模板链接起来,让它能被访问。在 async_api 目录下新建 urls.py:

列表 9.13 async_api/urls.py
from django.urls import pathfrom . import viewsapp_name = 'async_api'urlpatterns = [    path('', views.requests_view, name='requests'),]

然后修改项目主 urls.py,让它能路由到这个应用:

from django.contrib import adminfrom django.urls import path, includeurlpatterns = [    path('admin/', admin.site.urls),    path('requests/', include('async_api.urls'))]

最后,把 async_api 加入 INSTALLED_APPS:

INSTALLED_APPS = [    'django.contrib.admin',    'django.contrib.auth',    'django.contrib.contenttypes',    'django.contrib.sessions',    'django.contrib.messages',    'django.contrib.staticfiles',    'async_api']

准备完毕!启动应用:

gunicorn async_views.asgi:application -k uvicorn.workers.UvicornWorker

现在,打开这个页面:

http://localhost:8000/requests/?url=http://example.com&request_num=10

你将会看到一张漂亮的报表,如图 9.3 所示。

图 9.3:requests 异步视图

我们成功用 ASGI 部署了一个能并发发送大量请求的 Django 视图。

但问题是:如果项目不能用或不允许使用 ASGI,怎么办?比如你正在维护一个旧系统。好消息是,哪怕用 WSGI 运行,这个异步视图也依然可以工作!

尝试用 gunicorn 用 wsgi.py 运行:

gunicorn async_views.wsgi:application

你依然能访问这个接口。它为啥能“活”下来?

秘密就在于:在 WSGI 模式下,每次请求都会创建一个新的事件循环。你可以在视图里加一行代码来验证:

loop = asyncio.get_running_loop()print(id(loop))

你很快会发现,每次访问这个页面,id(loop) 输出的数字都不一样,说明每次请求都新建了一个事件循环。这恰好解决了阻塞的问题。

反观在 ASGI 模式下,id(loop) 每次都相同,因为整个应用只用一个全局的事件循环。

这告诉我们:即使不部署在 ASGI 上,也能利用异步的优势。但,如果某些逻辑需要跨请求维持一个长时间运行的事件循环,那就必须启用 ASGI。

9.4.1 在异步视图中运行阻塞代码

那如果视图里要用到传统的、同步的库呢?比如那些没有 await 支持的老旧代码,这可是“大忌”啊!好在 ASGI 有一招杀手锏:sync_to_async 函数。

在第 7 章,我们学过,可以把同步的代码扔进线程池执行,让它变成 awaitable。sync_to_async 就是做这个的“一键包装器”,但有个重要的细节:它默认是“线程敏感”(thread-sensitive)的。

很多同步库不是设计为多线程安全的,如果从多个线程调用,可能会引发竞态条件。为防万一,sync_to_async 默认会让代码在 Django 主线程里执行。这就意味着:任何你放进去的阻塞操作,都会阻塞整个 Django 应用进程——哪怕你想享受异步带来的好处,也得付出代价。

但如果你确定自己的代码是线程安全的(比如没有共享状态,或者共享状态不依赖特定线程),就可以将 thread_sensitive=False,它就会为每次调用创建一个新线程,避免阻塞主线程。

我们用个例子演示:

列表 9.14 sync_to_async 视图
from functools import partialfrom django.http import HttpResponsefrom asgiref.sync import sync_to_asyncdef sleep(seconds: int):    import time    time.sleep(seconds)async def sync_to_async_view(request):    sleep_time: int = int(request.GET['sleep_time'])    num_calls: int = int(request.GET['num_calls'])    thread_sensitive: bool = request.GET['thread_sensitive'] == 'True'    function = sync_to_async(partial(sleep, sleep_time), thread_sensitive=thread_sensitive)    await asyncio.gather(*[function() for _ in range(num_calls)])    return HttpResponse('')

然后在路由里加:

path('sync_to_async', views.sync_to_async_view)

现在用这个链接测试:

http://127.0.0.1:8000/requests/sync_to_async?sleep_time=5&num_calls=5&thread_sensitive=False

你会发现,总共只花了 5 秒 就完成(因为是并行运行),而且多次访问互不影响。现在把 thread_sensitive 设为 True,再来跑一遍:

  • 这次得花 25 秒,因为它要串行地运行五个 5 秒的睡眠。
  • 而且多次访问时,后一个请求必须等前一个完全结束才能开始,因为是阻塞了主线程。

所以,sync_to_async 是个利器,但也得讲究用法。你需要评估代码是否线程安全,也要权衡它对异步性能的影响。

9.4.2 在同步视图中调用异步代码

顺理成章的问题来了:“我有个同步的视图,但我想用 asyncio 库,该咋办?” 这时,ASGI 提供了另一件神兵利器:async_to_sync。

这个函数的作用是:把一个协程封装成一个同步函数,在需要的地方运行,返回结果。如果没运行事件循环(比如在 WSGI 下),它会自动创建一个;否则就在当前循环中执行。完美衔接!

我们试试把之前的 requests_view 改成同步视图,却依然用异步函数:

列表 9.15 在同步视图中调用异步代码
from asgiref.sync import async_to_syncdef requests_view_sync(request):    url: str = request.GET['url']    request_num: int = int(request.GET['request_num'])    context = async_to_sync(partial(make_requests, url, request_num))()    return render(request, 'async_api/requests.html', context)

然后添加路由:

path('async_to_sync', views.requests_view_sync)

访问:

http://localhost:8000/requests/async_to_sync?url=http://example.com&request_num=10

结果一模一样!

结论:就算在传统的同步(如 WSGI)环境下,只要善用 sync_to_async,你也能享受到一些异步架构带来的性能优势,而不必一步到位全部改造成异步。

总结

  • 我们学会了如何使用 aiohttp + asyncpg 构建连接数据库的基本的、强大的、可扩展的 RESTful API。
  • 学会了如何用 Starlette 构建完全兼容 ASGI 的现代化应用。
  • 学会了如何使用 Starlette 和 WebSocket,让你的前端能实时更新数据,彻底告别反复轮询的无聊重复劳动。
  • 学会了如何在 Django 中使用异步视图,以及如何在同步视图中调用异步代码,反向亦然,真正做到了融会贯通。

最新文章

随机文章