别把 async 当银弹:在 CPU 密集型图像处理服务中,优秀工程师为什么要敢于说“不”
别把 async 当银弹:在 CPU 密集型图像处理服务中,优秀工程师为什么要敢于说“不”
在 Python 编程里,asyncio、async/await、异步 I/O 这些词很有吸引力。它们听起来现代、优雅、高性能,也常常出现在高并发 Web 服务、实时消息推送、网络爬虫、网关服务和微服务架构中。
但真正成熟的工程师都知道:技术不是越新越好,而是越适合越好。
如果你的系统是一个纯 CPU 密集型图像处理服务,比如批量压缩图片、生成缩略图、做滤镜计算、图像识别预处理、像素级变换,有人坚持要“全量 async 重构”,这时我会明确拒绝。
不是因为我排斥异步,而是因为我理解异步。
一、先说结论:什么时候我会明确拒绝用异步?
当一个服务满足以下特征时,我会非常谨慎,甚至明确拒绝“全量 async 重构”:
- 主要耗时来自 CPU 计算,而不是网络、磁盘、数据库等 I/O 等待。
- 任务执行期间长时间占用 Python 解释器或底层计算资源。
- 系统瓶颈已经明确是 CPU 利用率打满。
- 现有问题可以通过多进程、任务队列、算法优化、C 扩展、GPU 加速解决。
- 引入 async 会显著增加代码复杂度,却不能带来对应收益。
- 团队缺少异步调试、压测、监控和异常处理经验。
- 重构目标模糊,只是因为“别人都在用 async”。
一句话:如果问题不是 I/O 等待,async 往往不是解药。
二、为什么 CPU 密集型任务不适合全量 async?
很多人误以为:
“async 是高并发,所以性能一定更好。”
这是一个常见误区。
异步编程擅长解决的是:程序在等待 I/O 时不要傻等。
比如:
importasyncioimportaiohttpasyncdeffetch(url):asyncwithaiohttp.ClientSession()assession:asyncwithsession.get(url)asresp:returnawaitresp.text()asyncdefmain():urls=["https://example.com","https://python.org","https://github.com",]results=awaitasyncio.gather(*(fetch(url)forurlinurls))print([len(r)forrinresults])asyncio.run(main())这个例子里,程序大部分时间在等待网络响应。等待期间,事件循环可以切换去处理其他请求,所以 async 很有价值。
但图像处理通常是另一种情况:
fromPILimportImage,ImageFilterdefprocess_image(input_path,output_path):image=Image.open(input_path)image=image.resize((800,600))image=image.filter(ImageFilter.SHARPEN)image.save(output_path)这类任务的主要成本是 CPU 计算、图像解码、像素处理、压缩编码。任务执行时,并不是“等别人返回”,而是本机 CPU 正在干活。
这时候你把它改成:
asyncdefprocess_image_async(input_path,output_path):image=Image.open(input_path)image=image.resize((800,600))image=image.filter(ImageFilter.SHARPEN)image.save(output_path)看起来用了async,但本质并没有变。函数内部没有真正释放事件循环的等待点,CPU 仍然被占满。甚至更糟:这个函数会阻塞事件循环,让其他协程也无法正常调度。
把同步 CPU 代码外面套一层 async,不叫异步优化,叫异步装饰。
三、一个错误的全量 async 重构示例
假设有一个图片批处理服务,原始版本如下:
frompathlibimportPathfromPILimportImage,ImageFilterdefresize_and_filter(path:Path,output_dir:Path):image=Image.open(path)image=image.resize((800,600))image=image.filter(ImageFilter.SHARPEN)output_path=output_dir/path.name image.save(output_path)defbatch_process(input_dir:str,output_dir:str):input_path=Path(input_dir)output_path=Path(output_dir)output_path.mkdir(exist_ok=True)forimage_pathininput_path.glob("*.jpg"):resize_and_filter(image_path,output_path)有人可能会说:“我们改成 async,就能并发处理了。”
于是写出这样的代码:
importasynciofrompathlibimportPathfromPILimportImage,ImageFilterasyncdefresize_and_filter(path:Path,output_dir:Path):image=Image.open(path)image=image.resize((800,600))image=image.filter(ImageFilter.SHARPEN)output_path=output_dir/path.name image.save(output_path)asyncdefbatch_process(input_dir:str,output_dir:str):input_path=Path(input_dir)output_path=Path(output_dir)output_path.mkdir(exist_ok=True)tasks=[resize_and_filter(image_path,output_path)forimage_pathininput_path.glob("*.jpg")]awaitasyncio.gather(*tasks)这段代码看起来并发了,实际上问题很多。
首先,resize_and_filter内部没有await。它并不会在执行中主动让出控制权。其次,图像处理逻辑仍然会占用 CPU。再次,一次性创建大量任务还可能带来内存压力。
更关键的是,它给团队制造了一种错觉:代码已经“异步高性能”了。
这种错觉很危险。
四、正确方向:CPU 密集型优先考虑多进程
对于 CPU 密集型任务,更合适的方案通常是:
frompathlibimportPathfromconcurrent.futuresimportProcessPoolExecutorfromPILimportImage,ImageFilterimportosdefresize_and_filter(args):input_file,output_dir=args image=Image.open(input_file)image=image.resize((800,600))image=image.filter(ImageFilter.SHARPEN)output_path=Path(output_dir)/Path(input_file).name image.save(output_path)returnstr(output_path)defbatch_process(input_dir:str,output_dir:str):input_path=Path(input_dir)output_path=Path(output_dir)output_path.mkdir(exist_ok=True)image_files=list(input_path.glob("*.jpg"))workers=max(os.cpu_count()-1,1)withProcessPoolExecutor(max_workers=workers)asexecutor:jobs=[(str(image_file),str(output_path))forimage_fileinimage_files]forresultinexecutor.map(resize_and_filter,jobs):print(f"processed:{result}")为什么这里用ProcessPoolExecutor?
因为 Python 中很多 CPU 密集型代码会受到 GIL 的影响。多线程适合 I/O 密集型任务,但 CPU 密集型任务往往需要多进程,让多个 Python 进程真正并行使用多个 CPU 核心。
当然,如果底层库本身已经释放 GIL,比如某些 NumPy、OpenCV、Pillow 内部操作,那线程也可能有收益。但工程判断不能靠猜,必须通过压测和 profiling 来验证。
五、async 不是完全不能用,而是不该全量滥用
在图像处理服务里,async 仍然可能有价值,但它应该用于合适的边界。
例如,一个完整请求可能包含:
- 接收 HTTP 请求。
- 从对象存储下载图片。
- 调用图像处理逻辑。
- 上传处理结果。
- 写入数据库记录。
- 返回任务状态。
其中,下载、上传、数据库访问属于 I/O 场景,适合异步;图像处理本身属于 CPU 场景,更适合进程池、任务队列或专用计算服务。
一个更合理的架构是:
HTTP API 层 | | 接收请求,快速校验 v 任务队列 | | 分发任务 v CPU Worker 进程池 | | 图像处理 v 对象存储 / 数据库如果使用 FastAPI,可以这样组织:
fromfastapiimportFastAPI,UploadFilefromconcurrent.futuresimportProcessPoolExecutorimportasyncioimportos app=FastAPI()pool=ProcessPoolExecutor(max_workers=max(os.cpu_count()-1,1))defheavy_image_process(file_path:str)->str:# 这里放真正 CPU 密集型图像处理逻辑# 例如 resize、filter、encode、AI preprocessingreturnf"processed-{file_path}"@app.post("/images")asyncdefupload_image(file:UploadFile):content=awaitfile.read()input_path=f"/tmp/{file.filename}"withopen(input_path,"wb")asf:f.write(content)loop=asyncio.get_running_loop()result=awaitloop.run_in_executor(pool,heavy_image_process,input_path)return{"result":result}这个方案里,API 层可以是 async,因为它要处理上传、读取、等待任务结果等 I/O 行为。但真正的 CPU 任务被丢进进程池,不阻塞事件循环。
这不是“拒绝 async”,而是把 async 放在它应该在的位置上。
六、判断是否使用 async 的工程清单
我通常会用一张非常直接的判断表。
| 问题 | 如果答案是“是” | 技术倾向 |
|---|---|---|
| 是否大量等待网络响应? | 是 | async / aiohttp / httpx |
| 是否大量等待数据库或缓存? | 是 | async DB driver 或连接池 |
| 是否主要做数学计算、图像处理、压缩编码? | 是 | 多进程 / C 扩展 / GPU |
| CPU 是否经常打满? | 是 | 优化算法、扩容 Worker、多进程 |
| 事件循环是否被阻塞? | 是 | 移走阻塞任务 |
| 团队是否能维护复杂 async 调用链? | 否 | 谨慎引入 |
| 改造目标是否可量化? | 否 | 暂停重构 |
一个优秀工程师不会问:“这个技术时不时髦?”
他会问:
“瓶颈在哪里?收益是什么?代价是什么?失败后如何回滚?”
七、如何用数据说服团队不要全量 async?
拒绝不是拍桌子。优秀工程师的“不”,必须建立在事实和专业判断之上。
我通常会做三件事。
第一,用 profiling 找瓶颈。
importcProfileimportpstatsdefmain():batch_process("./input","./output")if__name__=="__main__":profiler=cProfile.Profile()profiler.enable()main()profiler.disable()stats=pstats.Stats(profiler)stats.sort_stats("cumtime").print_stats(20)如果结果显示主要耗时都在图像解码、resize、filter、save,那么重构 async 并不能解决核心问题。
第二,做小规模基准测试。
importtimedefbenchmark(func,*args):start=time.perf_counter()func(*args)end=time.perf_counter()print(f"{func.__name__}:{end-start:.2f}s")分别测试:
同步单进程 线程池 进程池 伪 async 进程池 + async API 层第三,用压测结果讨论,而不是用情绪讨论。
关注这些指标:
平均响应时间 P95 / P99 延迟 CPU 使用率 内存占用 任务吞吐量 失败率 队列堆积长度 代码复杂度 排障成本如果“全量 async 重构”不能改善这些指标,就不应该成为优先方案。
八、比 async 更值得优先做的优化
在 CPU 密集型图像服务中,我会优先考虑这些方向。
1. 限制图片尺寸和输入质量
很多性能问题不是算法差,而是输入不可控。
fromPILimportImage MAX_WIDTH=2000MAX_HEIGHT=2000defvalidate_image(path):image=Image.open(path)width,height=image.sizeifwidth>MAX_WIDTHorheight>MAX_HEIGHT:raiseValueError("image is too large")returnimage2. 避免重复解码和重复保存
图片解码、编码很贵。如果中间流程频繁保存临时文件,性能会明显下降。
3. 使用批处理和任务队列
对于耗时任务,HTTP 请求不一定要同步等待完成。可以返回任务 ID,让后端 Worker 异步处理。
@app.post("/tasks")asyncdefcreate_task(file:UploadFile):# 保存文件# 投递任务到队列return{"task_id":"abc123","status":"queued"}4. 使用更合适的底层库
在一些场景下,OpenCV、NumPy、libvips、Rust/C++ 扩展、GPU 推理服务都可能比“把代码改成 async”更有效。
5. 做容量规划
如果单台机器 8 核 CPU,每个任务平均占用 1 个核心 500ms,那么吞吐上限是可以估算的。工程系统不是靠信仰扩容,而是靠模型和数据扩容。
九、优秀工程师为什么要敢于说“不”?
因为工程不是许愿池。
每一次技术选择,背后都有成本:
学习成本 重构成本 测试成本 排障成本 监控成本 团队交接成本 线上事故成本 机会成本“全量 async 重构”听起来很先进,但如果问题本质是 CPU 密集计算,它可能带来的是:
代码更难读 调用链更难追踪 异常更难处理 性能没有提升 线上问题更隐蔽 新人更难维护真正优秀的工程师不是永远说“可以”,而是能在关键时刻说:
“这个方向不解决主要矛盾,我们不应该这样做。”
这句话背后不是保守,而是负责。
对业务负责,对团队负责,对代码未来三年的维护者负责,也对凌晨两点被报警叫醒的自己负责。
十、怎么优雅地拒绝“全量 async 重构”?
拒绝也需要方法。
不要说:
“async 没用。”
可以说:
“async 对 I/O 密集场景非常有效,但我们当前瓶颈主要在 CPU 图像处理。全量 async 改造成本高,收益不确定。我建议先做 profiling 和小规模 benchmark。如果数据证明瓶颈在 I/O,我们再引入 async;如果瓶颈在 CPU,则优先采用进程池、任务队列和底层库优化。”
这是一种更专业的表达方式:既不否定技术,也不盲目跟风。
还可以提出替代方案:
第一阶段:profiling,确认瓶颈。 第二阶段:用进程池改造核心处理链路。 第三阶段:引入任务队列削峰。 第四阶段:API 层保留 async,用于文件上传、状态查询等 I/O 操作。 第五阶段:根据压测结果评估是否继续优化底层图像库。这比一句“不要用 async”更有建设性。
十一、一个可落地的推荐架构
对于纯 CPU 密集型图像处理服务,我推荐:
FastAPI / Flask API 层 | | 接收请求,做参数校验 v 消息队列 | | Celery / RQ / Dramatiq / Kafka v 多进程 Worker | | Pillow / OpenCV / libvips / NumPy v 对象存储 + 数据库 | | 记录状态和结果地址 v 客户端轮询或回调通知这个架构的优势是:
API 层轻量 CPU 任务隔离 Worker 可独立扩容 失败任务可重试 队列可削峰 监控指标更清晰在这个方案中,async 不是主角,但可以是配角。它可以用于 API 层处理并发连接,也可以用于状态查询、通知回调、对象存储访问。但它不应该强行接管 CPU 密集型核心逻辑。
十二、总结:不要迷信 async,要尊重问题本身
Python 编程的魅力,不在于把所有代码都写成最新范式,而在于用简洁、清晰、可靠的方式解决真实问题。
面对一个纯 CPU 密集型图像处理服务,我会明确拒绝“全量 async 重构”。原因很简单:
async 解决的是等待问题,不是计算问题。
优秀工程师敢于说“不”,不是为了显得强硬,而是为了保护系统不被错误方向拖入复杂泥潭。
真正的专业判断应该是:
I/O 密集:考虑 async CPU 密集:考虑多进程、算法优化、底层库、GPU 混合场景:分层治理,把 async 放在 I/O 边界 不确定:先 profiling,再 benchmark,最后决策技术世界变化很快,但有些原则不会过时:
先定位瓶颈,再选择方案。 先验证收益,再大规模重构。 先保护简单性,再追求先进性。愿你在每一次技术选型中,都不只是追逐潮流,而是成为那个能看清本质、守住质量、也敢于温柔而坚定地说“不”的工程师。
