Django REST中如何并行处理图片循环请求与用户注册请求?
Hey there! Let's break down why your requests are blocking each other and how to fix this properly.
问题根源
首先,Django的默认开发服务器(runserver)是单线程的。当你循环调用process_image接口时,每个请求都会占用这个唯一的线程——尤其是video.read()是一个阻塞操作,会让线程一直处于忙碌状态。这时候其他请求(比如用户注册)只能排队等待,直到图片请求停止、线程空闲出来才能被处理。
另外,你的AcceptImages视图里用了全局变量video,这在多线程/异步环境下是线程不安全的,很容易导致奇怪的资源冲突问题,后面我们也会一起解决这个。
解决方案一:用Celery实现异步处理(推荐)
Celery是Python的异步任务队列,专门用来处理这类耗时/阻塞的任务,把它们从web服务器的线程中剥离出来,让web服务器可以专心处理其他请求(比如注册)。下面是针对你的场景的具体实现步骤:
1. 安装依赖
先安装Celery和Redis(作为消息中间件,比RabbitMQ更易上手):
pip install celery redis
2. 配置Celery到你的Django项目
在项目根目录(和settings.py同级)创建celery.py文件:
import os from celery import Celery # 设置Django环境变量 os.environ.setdefault('DJANGO_SETTINGS_MODULE', '你的项目名称.settings') app = Celery('你的项目名称') # 从Django配置中读取Celery设置,前缀为CELERY_ app.config_from_object('django.conf:settings', namespace='CELERY') # 自动发现所有app中的tasks.py app.autodiscover_tasks()
然后在项目的__init__.py中添加:
from .celery import app as celery_app __all__ = ('celery_app',)
接着在settings.py末尾添加Celery的配置:
# Celery配置 CELERY_BROKER_URL = 'redis://localhost:6379/0' # Redis作为消息代理 CELERY_RESULT_BACKEND = 'redis://localhost:6379/0' # 存储任务结果
3. 把图片处理逻辑封装成Celery任务
在你的app目录下创建tasks.py文件,把原来的图片捕获逻辑移到这里:
from celery import shared_task import cv2 import base64 @shared_task def capture_image(): # 注意:不要用全局video!每次任务初始化摄像头,避免线程安全问题 video = cv2.VideoCapture(0) # 根据你的实际设备调整参数,比如视频文件路径 stat, img = video.read() video.release() # 记得释放摄像头资源 if not stat: return None # 捕获失败返回空 retval, buffer_img = cv2.imencode('.jpg', img) # 转成字符串返回,方便前端处理 resdata = base64.b64encode(buffer_img).decode('utf-8') return resdata
4. 修改视图,触发异步任务
更新AcceptImages视图,让它不再直接处理图片,而是触发Celery任务并返回任务ID:
from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from .tasks import capture_image class AcceptImages(APIView): def post(self, request): # 异步触发图片捕获任务 task = capture_image.delay() # 返回202 Accepted,告诉前端任务已开始 return Response({'task_id': task.id}, status=status.HTTP_202_ACCEPTED)
5. 添加获取任务结果的视图
前端需要通过任务ID轮询获取图片结果,所以新增一个视图:
from celery.result import AsyncResult from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status class GetImageResult(APIView): def get(self, request, task_id): task = AsyncResult(task_id) if task.state == 'SUCCESS': # 任务成功,返回图片数据 return Response({'image_data': task.result}) elif task.state == 'PENDING': # 任务还在处理中,返回状态 return Response({'status': 'Capturing image...'}, status=status.HTTP_202_ACCEPTED) else: # 任务失败或其他状态 return Response({'status': f'Task failed with state: {task.state}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
6. 更新URL配置
在urls.py中添加获取结果的路由:
url(r'^process_image/$', views.AcceptImages.as_view(), name='AcceptImage'), url(r'^image_result/(?P<task_id>[\w-]+)/$', views.GetImageResult.as_view(), name='GetImageResult'),
7. 前端调整
原来的循环调用process_image需要改成:
- 每次请求
process_image获取task_id - 循环请求
image_result/{task_id},直到拿到image_data - 展示图片后,重复步骤1-2继续获取下一张
8. 启动Celery Worker
最后,在终端启动Celery worker来处理异步任务:
celery -A 你的项目名称 worker --loglevel=info
解决方案二:用多线程服务器(快速缓解,不推荐生产环境)
如果暂时不想用Celery,可以用多线程的Django开发服务器,这样可以同时处理多个请求:
python manage.py runserver --threaded
不过这种方法只是缓解,因为video.read()的阻塞还是会占用线程,如果循环请求太频繁,还是会出现排队问题。而且全局video的线程安全问题依然存在,生产环境建议还是用Celery+专业服务器(比如Gunicorn+Nginx)。
关键注意事项
- 不要用全局变量
video:在多线程/异步环境下,全局变量会被多个请求/任务共享,导致摄像头资源冲突,必须在每个任务/请求中单独初始化并释放。 - 生产环境部署:生产环境不要用
runserver,要用Gunicorn或uWSGI配合多worker,再加上Celery处理异步任务,这样才能保证高并发和稳定性。
内容的提问来源于stack exchange,提问作者Muhammad Nabeel

