结论:i/o 密集任务用 threadpoolexecutor,cpu 密集任务用 processpoolexecutor;线程池适合 http 请求、文件读写等阻塞型操作,进程池适合数值计算、图像处理等 cpu 绑定任务,二者不可混用且子进程内不可再开线程池。

直接说结论:用 concurrent.futures.ThreadPoolExecutor 管理 I/O 密集任务,用 concurrent.futures.ProcessPoolExecutor 处理 CPU 密集任务;别混用,也别在子进程中再开线程池。
什么时候该选 ThreadPoolExecutor?
典型场景是发 HTTP 请求、读写文件、数据库查询——这些操作大部分时间在等系统调用返回,CPU 几乎空闲。线程池能复用线程、避免频繁创建销毁开销。
- 启动快,内存占用小(共享进程地址空间)
- 注意:如果任务里有
time.sleep()或阻塞式 IO,它确实能并发;但如果是纯计算(比如循环累加),GIL 会让多个线程串行执行 - 常见错误:把耗 CPU 的函数(如
math.factorial(10**5))丢进线程池,结果比单线程还慢 - 示例:
with ThreadPoolExecutor(max_workers=4) as executor: list(executor.map(requests.get, urls))
什么时候必须用 ProcessPoolExecutor?
当你有一堆独立的数值计算、图像处理、加密解密——这类任务会持续占用 CPU,且不依赖共享状态时,进程池才能真正并行。
- 绕过 GIL,多核利用率高
- 但进程启动慢、内存开销大(每个子进程复制父进程内存)
- 参数和返回值必须可序列化(
pickleable):不能传 lambda、嵌套函数、带绑定方法的对象;否则报AttributeError: Can't pickle local object - 子进程无法继承主线程的 logging 配置或全局变量,需显式传递或重置
submit() 和 map() 怎么选?
两者底层都用 Future,但接口和适用场景不同。
-
map():适合所有任务输入参数结构一致、顺序重要、且你愿意等全部完成再取结果。它自动按输入顺序返回结果,即使某些任务执行更慢 -
submit():适合需要提前处理部分完成的任务(比如超时控制、异常分类)、或参数不统一(不同函数、不同参数个数)。要用as_completed()才能按完成顺序获取结果 - 别对单个
submit()调用反复result()——它会阻塞;批量提交后统一result()或用as_completed()
为什么 max_workers 设太大反而更慢?
不是越多越好,尤其对进程池:操作系统调度开销、内存竞争、上下文切换都会拖慢整体吞吐。
- 线程池:通常设为 CPU 核心数的 2–4 倍(I/O 密集型),或干脆不设(默认
min(32, os.cpu_count() + 4)) - 进程池:一般设为
os.cpu_count()或略少(比如减 1),避免过度抢占 CPU - 真实瓶颈常不在 CPU/IO,而在外部服务限流(如 API 每秒最多 100 请求)——这时盲目加 worker 数只会触发 429 错误
- 调试技巧:先用
max_workers=1跑通逻辑,再逐步增加并观察响应时间和资源占用
最易被忽略的是:子进程无法访问主线程的 threading.local()、未显式初始化的模块级变量,以及任何基于文件描述符或 socket 的资源——它们不会自动继承,也不可跨进程共享。真要共享数据,得用 multiprocessing.Queue、Manager 或文件临时落盘。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











