threadpoolexecutor 适合 i/o 密集型任务,如网络请求、文件读写、数据库查询,因线程等待时会释放 gil;processpoolexecutor 才适用于 cpu 密集型任务,因其绕过 gil 实现真正并行。

选对执行器比调参重要得多——ThreadPoolExecutor 和 ProcessPoolExecutor 的性能差异主要来自任务类型,不是 max_workers 数值本身。
ThreadPoolExecutor 适合什么场景?
I/O 密集型任务用它才真正快,比如 requests.get、open()、数据库查询。因为线程在等待网络或磁盘响应时会自动释放 GIL,让其他线程继续跑。
- 常见错误现象:用
ThreadPoolExecutor跑纯计算(如sum(i*i for i in range(10**7))),速度反而比单线程慢 - max_workers 设置建议:通常 5–20 之间即可,设太高只会增加线程切换开销,不是越多越好
- 共享状态要小心:多个线程共用一个
list或全局变量时,必须加threading.Lock,否则结果错乱 - 示例中别漏掉
as_completed:想按完成顺序取结果,别用[f.result() for f in futures],它会严格按提交顺序阻塞等待
ProcessPoolExecutor 为什么 CPU 密集型任务必须用它?
Python 的 GIL 让多线程无法真正并行计算,而进程天然绕过 GIL。每个进程有独立内存,适合数值运算、图像处理、加密解密等。
- 常见错误现象:函数定义在
if __name__ == '__main__':外面,Windows/macOS 上直接报RuntimeError: An attempt has been made to start a new process - 参数传递有代价:每次
submit都要序列化(pickle)参数,大数据对象(如大 NumPy 数组)传参慢,建议用map+chunksize控制分块大小 - 进程启动开销大:
max_workers不宜设得远超 CPU 核心数(os.cpu_count()),否则空转进程反而拖慢整体 - 不能共享内存:想让所有进程更新同一个计数器?不行。得用
multiprocessing.Manager().dict()或队列通信
submit 和 map 的性能与使用边界
submit 灵活但手动管理多;map 简洁但要求函数签名统一、输入可迭代,且默认不支持 timeout。
-
executor.submit(task, x, y):适合参数各异、需单独处理异常的任务,每个返回一个Future -
executor.map(task, [a,b,c]):适合批量同构任务,返回迭代器,内部已做 chunk 分发,比循环 submit 快 10%–20% -
map的timeout参数只控制单次调用,不是整批超时;chunksize对ProcessPoolExecutor很关键——小数据设 1,大数据设 100+ 减少 IPC 次数 - 别在
map里传 lambda 或嵌套函数:进程池无法 pickle 它们,会报PicklingError
shutdown 和资源泄漏的真实风险
with 语句看似安全,但没处理好异常时,shutdown(wait=True) 可能卡死或跳过清理。
- 显式调用
executor.shutdown(wait=False)后立刻退出主程序?子进程/线程可能还在跑,变成孤儿进程 - 任务里抛了未捕获异常,
future.result()才会暴露,但若你忘了调用它,错误就静默丢失了 - 长时间运行的服务中,别复用同一个 executor 实例超过几小时:线程池可能积累僵尸线程,建议定期重建
- 进程池更敏感:如果子进程崩溃(如段错误),父进程可能 hang 在
wait上,加timeout并配合concurrent.futures.wait做兜底
真正影响性能的从来不是代码写了多少行,而是你有没有在任务刚定义时就判断清楚它是 I/O 还是 CPU 型——这个决定一旦错了,后面所有 max_workers 调优、chunksize 试探、异常捕获,都只是在给错误方向打补丁。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











