因为GIL限制多线程无法并行执行CPU密集型代码,而multiprocessing.Pool通过独立进程绕过GIL实现真正多核并行,但需权衡启动开销、序列化成本与内存占用。

为什么 Pool 对 CPU 密集型任务有效,而 threading 不行?
因为 Python 的全局解释器锁(GIL)会阻止多线程真正并行执行 CPU 密集型代码。而 multiprocessing.Pool 启动的是独立进程,每个进程有自己独立的 Python 解释器和内存空间,完全绕过 GIL —— 这才是能压满多核 CPU 的关键。
但代价也很明显:进程启动开销大、进程间数据传递需序列化(pickle)、内存占用翻倍。所以它只适合「单次计算耗时明显 > 进程通信开销」的场景,比如数值积分、图像滤波、蒙特卡洛模拟。
Pool.map 和 Pool.apply_async 该怎么选?
Pool.map 最简单,适合输入可迭代、函数纯且返回顺序必须严格对应输入顺序的场景。它会自动分块(chunksize 参数影响性能),但会阻塞直到全部完成。
Pool.apply_async 更灵活,适合需要异步控制、提前处理部分结果、或函数可能抛异常需单独捕获的情况。但它不保证执行顺序,返回的是 AsyncResult 对象,要用 .get() 才能取值 —— 这一步仍会阻塞。
立即学习“Python免费学习笔记(深入)”;
- 用
map:批量处理一批相似任务,比如对 1000 张图片统一做直方图均衡化 - 用
apply_async:提交几个不同参数组合的任务,边等边做日志或超时判断 - 别用
apply:它是同步调用,单进程排队执行,毫无并行意义
常见报错:PicklingError: Can't pickle XXX
这是最常卡住新手的问题。因为子进程无法访问主进程的闭包变量、lambda 表达式、类实例方法(未绑定)、或定义在 __main__ 模块外的嵌套函数。
解决办法很直接:
- 把目标函数定义在模块顶层(不能缩进,不能是类方法)
- 避免传入 lambda、闭包、或带状态的对象(如打开的文件句柄、数据库连接)
- 如果必须传复杂参数,拆成基本类型(
int/str/dict)或用functools.partial预设部分参数(注意 partial 本身也要可 pickle) - Windows 下必须加
if __name__ == '__main__':保护,否则会无限递归启新进程
怎么控制资源不炸掉?
Pool 默认进程数等于 os.cpu_count(),但实际中往往不是越多越好。尤其当任务本身内存占用高,或涉及磁盘/网络 I/O 时,盲目开满核反而导致频繁换页、缓存失效甚至 OOM。
建议显式指定 processes 参数:
- CPU 密集 + 小内存:设为
os.cpu_count() - 1,留一核给系统调度 - 单任务内存 > 1GB 或含 I/O:从 2–4 开始试,观察
htop或任务管理器的实际 CPU 利用率和内存增长 - 用
pool.close()+pool.join()显式回收,避免子进程僵尸化
另外,chunksize 在 map 中影响很大:太小 → 进程间通信频繁;太大 → 任务分配不均(某进程干完早,其他还在跑)。对万级输入,可设为 len(data) // (4 * processes) 作为起点。
真正难调的从来不是“怎么写”,而是“什么时候不该用”。比如一个 5ms 就算完的函数,哪怕跑百万次,用 Pool 往往比串行还慢 —— 这时候该想的是算法优化或向量化,而不是并行。


















