你写了个多进程脚本跑实验,满怀期待打开任务管理器——CPU 占用 20%,8 个核只亮了 2 个。进程确实起了 8 个,但跑得跟单线程一样慢。
这不是 Python 多进程的 bug,是你漏了 3 个关键参数。改完之后,实验从 6 小时缩到 45 分钟。
今天把 Python 多进程最常踩的坑一次讲透。不是教程,是实战排错——从"为什么我的多进程没用"到"让 CPU 真正跑满"的完整路径。
先搞清楚:Python 为什么需要多进程
Python 有个 GIL(全局解释器锁),同一个时刻只有一个线程在执行 Python 字节码。所以对 CPU 密集型任务(比如矩阵运算、模型训练、大规模数据处理),多线程基本没用,必须用多进程。
科研场景大部分是 CPU 密集型——跑模型、做参数搜索、处理大规模数据。所以今天的主角是 multiprocessing。
为什么你的多进程"没用"?3个参数排查
问题复现:看着像多进程,实际还是串行
先看一段"看起来没问题"的代码:
from multiprocessing import Pool import time def process_data(params): # 模拟CPU密集型任务 result = sum(i * i for i in range(10**7)) return result if __name__ == "__main__": params_list = list(range(100)) start = time.time() with Pool() as pool: results = pool.map(process_data, params_list) print(f"耗时: {time.time() - start:.1f}s")你跑一下,发现 100 个任务耗时和串行差不多。打开任务管理器,CPU 占用 20%。问题出在哪?
参数1:进程数没指定(默认值可能不是你的CPU核数)
Pool() 不传参数时,默认进程数是 os.cpu_count()。听起来没问题?但在 Windows 上,如果你在 Jupyter Notebook 或某些环境下运行,cpu_count() 可能返回逻辑核数而非物理核数,甚至返回 None。
更关键的是——进程数不是越多越好。对于纯 CPU 密集型任务,最优进程数 = 物理核数。超过这个数反而因为进程切换开销变慢。
import os import psutil # pip install psutil # 获取物理核数(不是逻辑核数) physical_cores = psutil.cpu_count(logical=False) print(f"物理核数: {physical_cores}") # CPU密集型任务:进程数 = 物理核数 # IO密集型任务:进程数 = 物理核数 × 2~4 with Pool(processes=physical_cores) as pool: results = pool.map(process_data, params_list)实操建议:纯计算任务,进程数 = 物理核数。如果任务里有 IO(读文件、网络请求),可以适当增加到 1.5-2 倍。但别超过逻辑核数——超了就是浪费。
参数2:chunksize 没调(任务分发太碎)
这是最多人忽略的参数。pool.map(func, iterable) 默认 chunksize=1,意味着每完成一个任务,进程都要回主进程领下一个任务。100 个任务就要 100 次进程间通信。
进程间通信的开销比你想象的大——序列化、反序列化、队列进出,每个任务可能多花 5-10ms。100 个任务就是 0.5-1s 纯浪费。
# ❌ 慢:每个任务单独分发 results = pool.map(process_data, params_list) # ✅ 快:每批分发 chunksize 个任务 # 经验公式:chunksize = len(iterable) // (processes * 4) n_tasks = len(params_list) n_procs = physical_cores chunksize = max(1, n_tasks // (n_procs * 4)) results = pool.map(process_data, params_list, chunksize=chunksize)
实测对比(8核机器,100个任务,每个任务约3秒):
参数3:maxtasksperchild 没设(内存泄漏累积)
多进程跑长时间任务(比如参数搜索跑几千组),有个隐形杀手:每个子进程的内存只增不减。Python 的垃圾回收不是实时的,长时间运行的子进程可能累积几百 MB 垃圾。
maxtasksperchild 参数让每个子进程完成指定数量任务后自动重启,释放内存。
# 每个进程完成50个任务后重启,防内存泄漏 with Pool(processes=physical_cores, maxtasksperchild=50) as pool: results = pool.map(process_data, params_list, chunksize=chunksize)
这个参数对跑几千组参数搜索的科研场景特别重要。不设的话,跑到第 500 组可能内存就爆了(上期讲的内存泄漏问题,多进程场景更容易触发)。
科研实战:参数搜索并行化模板
下面是一个可以直接用的科研参数搜索模板,把上面 3 个参数都用上:
import os import time from multiprocessing import Pool import psutil def run_experiment(config): # 单次实验: config 是参数字典 # 返回: 结果字典 # 你的实验代码 lr = config['lr'] batch_size = config['batch_size'] epochs = config['epochs'] # ... 训练模型 ... accuracy = 0.85 # 模拟结果 return { 'config': config, 'accuracy': accuracy, 'time': time.time() } if __name__ == "__main__": # 1. 生成参数空间(网格搜索) configs = [] for lr in [0.001, 0.01, 0.1]: for bs in [32, 64, 128]: for ep in [10, 50, 100]: configs.append({'lr': lr, 'batch_size': bs, 'epochs': ep}) print(f"共 {len(configs)} 组实验") # 2. 配置多进程 n_cores = psutil.cpu_count(logical=False) chunksize = max(1, len(configs) // (n_cores * 4)) print(f"进程数: {n_cores}, chunksize: {chunksize}") # 3. 并行执行 start = time.time() with Pool(processes=n_cores, maxtasksperchild=10) as pool: results = pool.map(run_experiment, configs, chunksize=chunksize) print(f"总耗时: {time.time() - start:.1f}s") # 4. 找最优配置 best = max(results, key=lambda x: x['accuracy']) print(f"最优配置: {best['config']}, 准确率: {best['accuracy']}")这个模板覆盖了 90% 的科研并行场景——超参搜索、交叉验证、多数据集对比、消融实验。把 run_experiment 函数替换成你的实验代码就能用。
3个常见坑
- 坑1:Windows 下 if __name__ == "__main__" 必须写。 Windows 的多进程用 spawn 方式启动子进程,子进程会重新 import 主模块。不写这行会导致无限递归创建子进程。Jupyter Notebook 里跑多进程经常报错也是这个原因——建议把多进程代码写成 .py 文件单独运行。
- 坑2:大对象通过参数传递。
pool.map 的参数会被序列化(pickle)后传给子进程。如果你传一个 2GB 的 DataFrame,每个子进程都要反序列化一份——内存直接乘以进程数。解决方案:用 initializer 在子进程启动时加载数据,或用共享内存(multiprocessing.shared_memory,Python 3.8+)。 - 坑3:用 Pool 跑需要 GPU 的任务。 多进程和多 GPU 不天然兼容。如果你的实验用 PyTorch/TensorFlow,每个子进程会尝试占用所有 GPU 显存。解决方案:在子进程开头用
os.environ['CUDA_VISIBLE_DEVICES'] 指定单张卡,或者直接用框架自带的 DataParallel / DistributedDataParallel。
免费说明
multiprocessing 是 Python 标准库,完全免费。psutil 是第三方库,pip install psutil 即可,也是免费开源的。
如果你的数据量特别大(超过内存),可以考虑 joblib(scikit-learn 团队开发,支持内存映射 + 并行)或 Ray(分布式计算框架,支持多机并行)。但 90% 的科研场景,multiprocessing 足够了。
快速自查清单
如果你正在写多进程代码但感觉"没快多少",对着这个清单逐项检查:
- □ 进程数是否 = 物理核数?(不是逻辑核数,不是默认值)
- □ chunksize 是否 > 1?(至少 len(iterable) // (processes * 4))
- □ maxtasksperchild 是否设置?(长时间任务建议 10-50)
- □ 传给子进程的参数是否过大?(避免传整个 DataFrame)
- □ Windows 下是否写了 if __name__ == "__main__"?
- □ CPU 密集型还是 IO 密集型?(IO 密集型用 asyncio 可能更合适)
- □ 是否有全局锁或共享资源拖慢并行?(尽量让子进程独立运行)
多进程不是万能药。如果你改完上面 3 个参数后发现加速比还是不理想,可能瓶颈不在 CPU——而是磁盘 IO(读数据太慢)或内存带宽(数据太大,CPU 等数据)。这时候需要换思路:用内存映射文件、减少数据拷贝、或者上 SSD。
关注后回复【多进程】,领取:
- 多进程参数搜索完整模板代码(含进度条 + 结果自动保存)
- chunksize 计算器(输入任务数和核数,自动算最优值)
- 多进程常见报错排查表(Windows/macOS/Linux 分别处理)
- 共享内存使用示例(Python 3.8+ multiprocessing.shared_memory)
你跑多进程时遇到过什么坑?是 CPU 跑不满、内存爆炸、还是进程死锁?评论区描述你的问题,我帮你分析原因。
这是「科研人效率工具系列」第 20 期,上期:PPT被导师打回3次?用AI 5分钟把论文变成答辩级幻灯片。下期预告:导师让一周交文献综述?用AI 3天从0到成稿的全流程。
— 关注「科研创新社」,每天一个科研效率提升技巧 —