提示

返回博客列表

下载器并发模型踩坑:线程池、协程还是多进程?(压测实录)

下载器并发模型踩坑:线程池、协程还是多进程?(压测实录)

写下载器绕不开一个问题:同时下 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 分开,比在单一模型里调参有用得多。

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部