下载器并发模型踩坑:线程池、协程还是多进程?(压测实录)
写下载器绕不开一个问题:同时下 10 个、100 个任务,底层到底用线程、协程还是多进程?我最早用 concurrent.futures.ThreadPoolExecutor 一把梭,后来遇到 CPU 飙高、界面卡死、内存爆掉,才认真做了一次压测。结论有点反直觉,记下来给后来人——尤其给那些"无脑开 200 线程"的兄弟泼盆冷水。
一、先定性:下载是 IO 密集
下载任务 90% 时间在等网络,CPU 基本闲着。理论上线程或协程都行,多进程是杀鸡用牛刀。但"理论上"和"实际上"差了一个 GIL。
Python 的 GIL(全局解释器锁)在纯 IO 等待时会释放,所以多线程下载确实能并发。可一旦你在回调里做了解析、解密、转码(这些吃 CPU),GIL 就让多线程退化成"看起来并发、实际串行"——多个下载任务的 CPU 部分排队执行,下载线程被占满,新任务饿死。
二、压测环境与方法
- 任务:100 个 50MB 视频,分两种模式测:
- 纯下载:下完即丢,无后处理;
- 下载+后处理:下完做哈希校验 + 轻量转码(模拟真实场景)。
- 机器:4 核 8 线程笔记本,千兆内网(避免公网带宽成变量)。
- 每种模型跑 3 次取中位数。
三、纯下载结果
| 模型 | 并发数 | 总耗时 | 峰值内存 | 备注 |
|---|---|---|---|---|
| 线程池 | 16 | 92s | 320 MB | 最稳 |
| 线程池 | 64 | 88s | 1.1 GB | 收益见顶、内存涨 |
| 线程池 | 200 | 95s | 2.8 GB | 被远端限流 429,更慢 |
| asyncio+aiohttp | 16 | 90s | 280 MB | 与线程持平 |
| asyncio+aiohttp | 64 | 85s | 300 MB | 略好,代码更复杂 |
| 多进程 | 8 | 95s | 2.3 GB | 反而最慢最重 |
结论一:并发数到 16 之后再加几乎没收益。 瓶颈是带宽和远端限流,不是你的并发模型。我一度以为开 200 线程能起飞,结果被服务器限流直接 429,反而更慢、还被拉黑。
结论二:协程在纯下载场景略优,但优势小到不值得重写。 你已经在线程池上跑得好好的?别折腾 asyncio。
结论三:多进程最差。 进程间传数据开销大,下载又不需要多核计算,纯属徒增内存。
四、加上后处理,剧情反转
真实下载器下载完要:校验哈希、解密(如果有)、转码、写数据库。这些 CPU 活儿一旦进主流程,多线程原形毕露——后处理排队,下载线程被占满、下载队列饿死。此时:
- 纯线程池:总耗时从 92s 飙到 210s,且 CPU 占用长期 100%(GIL 串行化后处理)。
- 混合模型(线程下载 + 进程池后处理):下载用线程(IO 友好),重 CPU 后处理丢进
ProcessPoolExecutor(绕开 GIL),总耗时 110s,CPU 和 IO 各司其职,互不阻塞。
这就是我现在的架构:下载与计算分离。
五、一个简化架构示意
┌─────────────┐ (下载线程池, 16) ┌──────────────┐
│ 任务队列 │ ───────────────────▶ │ 落盘 .tmp │
└─────────────┘ └──────┬───────┘
│ 下载完成事件
▼
┌────────────────┐
│ 进程池(后处理) │ 校验/转码/写DB
└───────┬────────┘
▼
完成队列 / UI
关键点:后处理失败时只重试后处理,不重下整个文件(用落盘的 .tmp 复用)。这避免了一处报错就从头再下的浪费。
六、反压(backpressure)很重要
并发数拉太高还会触发另一问题:所有任务同时开始 → 瞬间占满带宽 → 每个都变慢 → 整体超时。我加了一层信号量限制"同时在途任务数",并对失败任务做指数退避:
sem = asyncio.Semaphore(16)
async def worker(task):
async with sem:
await download(task)
GUI 场景下还要把进度回调节流(每 100ms 合并一次 UI 更新),否则高频 signal.emit 直接把主线程卡死——这是另一个常见"界面卡死"真凶。
七、给选型的一点建议
- 单纯下载/请求:线程池,
max_workers设 16~32,别盲目拉高。 - 高并发小请求(批量探测链接):asyncio 更优雅,但收益有限。
- 有重 CPU 后处理:线程管 IO + 进程管计算,混合模型。
- 永远给用户一个"最大并发"滑块,并默认保守值。我见过有人默认开 50,结果自己宽带被占满、其他啥都干不了。
- 限流与退避不能省:429/5xx 要退避重试,别硬刚。
八、一句话总结
并发模型没有银弹,先测再选。我那次"多线程卡死"的真相,最后发现不是模型选错,而是后处理把线程占满、下载队列饿死——换个架构布局比换语言管用。把 IO 和 CPU 分开,比在单一模型里调参有用得多。