作为最流行、使用最广泛的 Python 框架之一,Django 出厂即带着一堆实用功能:强悍的 对象关系映射器(ORM)帮你操控数据库,自带的可定制后台管理系统让你效率飞升。
过去,直到版本 3.0 以前,Django 仅支持以 WSGI 方式部署,几乎不支持异步。主要依靠第三方的 channels 库来满足部分需求。但从 3.0 版本开始,它正式引入了对 ASGI 支持,开启了向全异步演进的大门。到了 3.1 版本,更是实现了异步视图,让开发者能直接在视图中使用 asyncio 语言。
当然,这还处于早期阶段。比如,它的数据库引擎仍保持同步(但未来的计划是支持异步),整体能力还在不断完善。不过别担心,随着 Django 不断进化,这块短板会逐渐补齐。
我们来实战一下:用 Django 构建一个视图,去调用 aiohttp 进行并发请求。设想你正在集成一个外部 API,需要跑一批并发请求,查看每个请求的响应时间、返回体长度,还有失败次数。我们希望做一个页面,接收一个目标网址和请求数,然后并发发起请求,最后把结果以表格形式返回。
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
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:
<!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
我们成功用 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。
那如果视图里要用到传统的、同步的库呢?比如那些没有 await 支持的老旧代码,这可是“大忌”啊!好在 ASGI 有一招杀手锏:sync_to_async 函数。
在第 7 章,我们学过,可以把同步的代码扔进线程池执行,让它变成 awaitable。sync_to_async 就是做这个的“一键包装器”,但有个重要的细节:它默认是“线程敏感”(thread-sensitive)的。
很多同步库不是设计为多线程安全的,如果从多个线程调用,可能会引发竞态条件。为防万一,sync_to_async 默认会让代码在 Django 主线程里执行。这就意味着:任何你放进去的阻塞操作,都会阻塞整个 Django 应用进程——哪怕你想享受异步带来的好处,也得付出代价。
但如果你确定自己的代码是线程安全的(比如没有共享状态,或者共享状态不依赖特定线程),就可以将 thread_sensitive=False,它就会为每次调用创建一个新线程,避免阻塞主线程。
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 是个利器,但也得讲究用法。你需要评估代码是否线程安全,也要权衡它对异步性能的影响。
顺理成章的问题来了:“我有个同步的视图,但我想用 asyncio 库,该咋办?” 这时,ASGI 提供了另一件神兵利器:async_to_sync。
这个函数的作用是:把一个协程封装成一个同步函数,在需要的地方运行,返回结果。如果没运行事件循环(比如在 WSGI 下),它会自动创建一个;否则就在当前循环中执行。完美衔接!
我们试试把之前的 requests_view 改成同步视图,却依然用异步函数:
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 中使用异步视图,以及如何在同步视图中调用异步代码,反向亦然,真正做到了融会贯通。