在开发 Django 异步视图时,经常会看到这样的代码:
python
class UserDataView(View):
async def get(self, request):
# 权限校验
user_id = request.GET.get('user_id')
if not user_id:
return JsonResponse({'error': '缺少 user_id'}, status=400)
# 查询用户(同步 ORM 必须用 sync_to_async 包装)
user = await sync_to_async(User.objects.filter(id=user_id, is_active=True).first)()
if not user:
return JsonResponse({'error': '用户不存在'}, status=404)
# 查询用户的订单(同样需要包装)
orders = await sync_to_async(list)(Order.objects.filter(user=user, status='paid'))
# 返回数据
return JsonResponse({
'username': user.username,
'orders_count': len(orders),
})
很多同学会产生疑问:
-
为什么要用
sync_to_async包装User.objects.filter和list? -
直接在异步视图里调用这些同步 ORM 方法会有什么问题?
-
协程和异步到底体现在哪里?
async def究竟有什么作用?
本文将从这段代码出发,逐步剖析 Django 异步编程的核心机制。
1. 异步视图与事件循环
Django 的异步视图使用 async def 定义,例如:
python
async def get(self, request):
# ...
async def 定义了一个协程函数 ,调用它会返回一个协程对象 ,而不是立即执行。这个协程对象会被交给事件循环(event loop)进行调度。
事件循环 是一个运行在单线程 中的调度器,它维护着一个就绪队列(Ready Queue),里面放着所有可以执行的协程(Task)。事件循环会不断从队列中取出协程执行,直到遇到 await 挂起点,然后切换去执行下一个协程。
这种机制就是协作式并发:协程主动让出 CPU,事件循环得以在多个任务间快速切换,从而实现高并发。
2. 为什么不能直接调用同步 ORM?
Django 默认的 ORM 方法是同步阻塞的。如果在异步视图中直接调用:
python
user = User.objects.filter(id=user_id).first() # 阻塞
orders = list(Order.objects.filter(user=user)) # 阻塞
那么当前线程(即事件循环线程)会被这些同步操作完全占用,直到数据库查询完成。在此期间,事件循环无法处理任何其他请求,整个服务的并发能力瞬间退化到同步模式。
这就是阻塞事件循环的后果。
3. sync_to_async 的作用
sync_to_async 是 Django 提供的一个工具,用于将同步函数 包装成可等待的异步函数 。它的内部实现会将同步函数的执行提交到线程池 (默认是 ThreadPoolExecutor)中,从而避免阻塞事件循环线程。
因此,正确写法应该是:
python
user = await sync_to_async(User.objects.filter(id=user_id).first)()
orders = await sync_to_async(list)(Order.objects.filter(user=user))
执行流程:
-
事件循环线程执行到
await sync_to_async(...),将同步任务提交给线程池,然后立刻挂起当前协程。 -
事件循环从队列中取出下一个就绪协程继续执行,实现并发。
-
线程池中的某个工作线程执行
User.objects.filter(...).first()和list(...),完成后将结果返回。 -
事件循环收到结果通知,恢复原来的协程继续执行。
通过这种方式,事件循环线程不会被阻塞,可以同时处理大量并发请求。
4. 协程与线程池的关系
很多初学者会混淆协程和线程池,甚至认为"既然用了线程池,就不需要异步了"。实际上,两者的角色完全不同:
| 概念 | 角色 | 运行位置 |
|---|---|---|
| 协程 | 执行单元,可挂起/恢复 | 事件循环线程(单线程) |
| 事件循环 | 调度器,管理多个协程 | 主线程 |
| 线程池 | 辅助执行同步阻塞任务 | 独立的工作线程 |
协程本身并不在线程池中运行 。协程始终运行在事件循环线程上,只是通过 await 让出控制权,等待线程池的结果。线程池的作用是把"脏活累活"(同步 I/O)移出事件循环线程,避免阻塞。
5. 异步与协程的本质
协程的核心理念是协作式多任务 :一个协程遇到 await 时主动挂起,事件循环可以立即切换到其他就绪的协程。这种模型具有以下优势:
-
轻量级:协程的创建和切换开销远小于操作系统线程。
-
无竞态条件:由于所有协程运行在同一个线程中,无需考虑锁、原子操作等同步问题(前提是协程内不使用多线程)。
-
高并发:单线程可以轻松管理成千上万个协程,而线程数通常受限于系统资源。
在 Django 异步视图中,每个请求的视图函数被包装成一个协程,由事件循环调度。当大量请求同时到达时,事件循环可以快速在它们之间切换,而不会因为线程数不足导致排队。
6. 如果没有 async def 会怎样?
如果视图函数不使用 async def,而是普通同步函数:
python
def get(self, request):
user = User.objects.filter(id=user_id).first()
orders = list(Order.objects.filter(user=user))
那么 Django 会在一个同步线程中执行该函数,内部无法使用 await,即使想用 sync_to_async 也无法享受到协程调度的好处。这种模型的并发能力受限于工作线程数量(通常几十个),高并发下性能较差。
7. 总结
-
async def定义协程函数,允许使用await进行非阻塞等待,由事件循环调度。 -
事件循环 运行在单线程上,管理多个协程,通过协作式切换实现高并发。
-
同步 ORM 是阻塞的,必须用
sync_to_async包装,提交到线程池执行,避免阻塞事件循环。 -
线程池 是辅助工具,用于执行同步阻塞任务,协程本身不在线程池中运行。
-
整个异步模型的核心是 协程 + 事件循环,线程池只是解决同步库兼容问题的桥接方案。
理解这些原理后,你就能更自信地编写高效、可扩展的 Django 异步应用了。
参考资料: