文章目录
- 多线程、多进程、协程——决策树与 Django 项目中的并发选型实践(下)
- 导入语
- 1 ~> 终极决策树
- 2 ~> Django 并发选型问题一:Celery Worker 用多进程还是协程
- 2.1 默认是 prefork(多进程)
- 2.2 换成 gevent(协程池)
- 2.3 什么时候用 prefork vs gevent
- 3 ~> Django 并发选型问题二:Gunicorn Worker 类型
- 3.1 真实经历
- 4 ~> Django 并发选型问题三:异步 View vs 同步 View
- 思考 && 总结
- 结尾
多线程、多进程、协程——决策树与 Django 项目中的并发选型实践(下)
📖文章简介:上篇用三组 benchmark 实测了多线程、多进程、协程的纯 IO / 纯 CPU / 混合负载性能。下篇基于这些数据给出一张可直接使用的决策树——拿到需求先判断瓶颈类型(IO or CPU),再看数据共享需求,最后选模型。深入 Django 项目中的实际选型:Celery 任务用多进程还是协程?Django View 本身需要并发吗?Gunicorn 的 worker 类型(sync / gevent / gthread)怎么选?每个问题都对应一个真实的 Django 优化经历——从 celery worker 从 4 进程改成 gevent 协程池后内存从 2GB 降到 400MB。
🎬 个人主页:源码骑士
❄专栏传送门:《Android开发基础》《python基础课程》
⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂
🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"
导入语
上篇的三组 benchmark 给了数据,下篇直接给决策树。另外把 Django 项目中和并发模型选择紧密结合的几个实际问题拆开——Celery、Gunicorn worker、异步 View——每选错一次就是一次生产事故。
1 ~> 终极决策树
拿到性能需求 │ ├─ 到底是慢在哪里?先做基准测试! │ │ (别猜——用 cProfile / Debug Toolbar 跑一次再说) │ │ │ ├─ IO 密集型(网络 / 磁盘 / 数据库查询占绝大部分) │ │ ├─ 需要和现有同步库(Django ORM、requests)互操作 │ │ │ └─ → 多线程 ThreadPoolExecutor ✅ │ │ │ │ │ ├─ 全链路可从零用异步库写(aiohttp、asyncpg、httpx) │ │ │ ├─ 并发量<500→ 多线程也可以 │ │ │ └─ 并发量>1000→ 协程 asyncio ✅ │ │ │ │ │ └─ 数据量大且不能共享内存 → 多进程 + Queue │ │ │ ├─ CPU 密集型(纯计算:图像处理、数值运算、正则大匹配) │ │ ├─ 计算简单且数据量小 → 多进程池 ProcessPoolExecutor ✅ │ │ ├─ 库是 NumPy/Numba/Cython → 多线程也可以(C 层释放 GIL) │ │ └─ 计算极端重量 → 分离任务队列(Celery + 独立 worker) │ │ │ └─ 混合负载(既有 IO 又有 CPU) │ ├─ IO 占比>70% → 多线程 ✅ │ ├─ CPU 占比>70% → 多进程 ✅ │ └─ IO/CPU 各占一半 → 多线程(给 CPU 部分也用线程池)2 ~> Django 并发选型问题一:Celery Worker 用多进程还是协程
2.1 默认是 prefork(多进程)
celery-Amyproject worker--concurrency=4# 默认 prefork——4 个独立的进程,每个进程有自己的一份内存优点:稳定,不受 GIL 影响。缺点:每个进程加载整个 Django app——4 个进程 = 4 倍的 Django 初始化内存。
2.2 换成 gevent(协程池)
pipinstallgevent celery-Amyproject worker--pool=gevent--concurrency=500500 个协程共享一个 Django 实例——内存从 2GB 降到 400MB。但要求 Celery 任务本身是 IO 密集型的——如果是 CPU 计算,gevent 不会有性能收益。
2.3 什么时候用 prefork vs gevent
| 任务类型 | 推荐 pool | 原因 |
|---|---|---|
| 发送邮件、推送通知 | gevent | 纯 IO,协程最优 |
| 图片压缩、PDF 生成 | prefork | CPU 密集型,协程无收益 |
| 调用第三方 API(大量) | gevent | 大量等待,协程发挥优势 |
| 数据库批量写入 | prefork | 批量写也可能有 DB 层锁,prefork 避免阻塞 |
3 ~> Django 并发选型问题二:Gunicorn Worker 类型
| worker 类型 | 并发模型 | 适合场景 |
|---|---|---|
sync(默认) | 一个 worker 一个请求 | 低并发、CPU 密集型 |
gevent | 协程——一个 worker 处理成千上万个连接 | IO 密集型高并发 |
gthread | 一个 worker 多线程 | 需要和 Django 同步代码互操作的 IO 场景 |
# 高并发 API(IO 型)——用 geventgunicorn myproject.wsgi --worker-class=gevent--workers=4--worker-connections=1000# CPU 密集型或对同步性要求严格的——用 syncgunicorn myproject.wsgi--workers=43.1 真实经历
2022 年把 API 网关从sync换成gevent后,单台 2 核 4GB 服务器能承载的并发连接数从 200 提升到 3000。但发现有一个使用了requests同步库的 View 在 gevent 下没有释放控制权——必须换上grequests(gevent 版 requests),或给该 View 开单独的路由组。
4 ~> Django 并发选型问题三:异步 View vs 同步 View
Django 3.1+ 支持异步 View。但 Django ORM 本身不支持异步——await Book.objects.filter()不存在。
# ❌ 异步 View 中直接调 ORMasyncdefbook_list(request):books=Book.objects.all()# 同步调用——卡死事件循环returnJsonResponse({"books":list(books.values())})# ✅ 用 sync_to_async 包装fromasgiref.syncimportsync_to_asyncasyncdefbook_list(request):books=awaitsync_to_async(list)(Book.objects.all().values())returnJsonResponse({"books":books})思考 && 总结
Django 并发选型的三条原则:
- **先确定瓶颈(IO/CPU)**再选模型。数据驱动——别听别人说"多线程没用"就放弃多线程。
- Celery 任务类型决定 worker pool。IO → gevent(省内存),CPU → prefork(稳定)。
- Gunicorn worker class 影响面最大。gevent 提高 IO 并发,sync 更稳定——根据业务选。
结尾
并发模型对比上下篇完结。感谢阅读!
源码骑士 — 源码级拆解,从底层看透技术
👀关注:跟博主一起从源码视角深耕底层原理
❤️点赞:让优质内容被更多人看见
⭐收藏:核心知识点存好,随用随查
💬评论:分享你的经验或疑问,一起交流
🔄一键四连:别忘了给博主一键四连!
🗡️寄语:选模型不要听口碑——看实测数据,看业务特征。
结语:决策树放在手边,以后拿到并发优化需求就不慌。下篇进入 Redis 实战。一键四连!