joblib通过Parallel(进程池调度器)与delayed(参数包装器)组合实现并行,要求函数为顶层可导入、参数可序列化、返回值不宜过大;n_jobs=-1启用全部核心,Windows下必须加if name == '__main__':保护。

joblib 是 Python 中最轻量、最贴近实际工程需求的并行计算工具之一,尤其适合 CPU 密集型任务(比如数值模拟、特征提取、模型预测)。它不依赖复杂配置,几行代码就能把串行 for 循环变成多进程并行——但前提是你清楚哪些地方不能并行、哪些参数一设错就白忙活。
为什么直接用 Parallel + delayed 就能并行?
Parallel 本质是进程池调度器,delayed 是一个“占位包装器”:它不执行函数,只记录函数名和参数,等 Parallel 启动 worker 进程后,再把函数体和参数一起序列化过去执行。
- 它要求被并行的函数必须是模块顶层可导入的(不能是嵌套函数、lambda、类方法),否则会报
Can't pickle local object - 所有参数必须能被
joblib序列化(numpy数组、基本类型、dict/list没问题;含文件句柄、线程锁、数据库连接的对象会失败) - 返回值也会被反序列化回主进程,所以别返回超大中间结构(比如每个子任务返回一个 1GB 的
ndarray)
示例中常见写法:
from joblib import Parallel, delayed <p>def process_item(x): return x ** 2 + 1</p><p>results = Parallel(n_jobs=4)(delayed(process_item)(i) for i in range(10))
注意:delayed(process_item)(i) 不是调用,而是构造一个待执行任务;括号外的 Parallel(...)(...) 才真正触发并行执行。
立即学习“Python免费学习笔记(深入)”;
n_jobs 设多少才合理?
n_jobs 不是越大越好,设错反而拖慢:
-
n_jobs=-1:启用全部逻辑核心(推荐用于纯计算、无 I/O 瓶颈的任务) -
n_jobs=-2:留一个核心给系统或其他进程(更稳妥,尤其在笔记本或共享服务器上) -
n_jobs=1:强制串行(调试时有用,避免多进程干扰 print 或日志) -
n_jobs>CPU 核心数:不会加速,还增加进程调度开销;对 I/O 密集型任务甚至可能更慢
另外要注意:如果你的任务本身含大量磁盘读写(比如每轮都 pd.read_csv 一个大文件),backend='threading' 可能比默认的 'loky' 更合适——因为 GIL 在 I/O 时会释放,线程切换开销小。但 CPU 密集型任务必须用多进程('loky' 或 'multiprocessing'),线程无效。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
如何避免重复计算 + 并行加速同时生效?
joblib.Memory 和 Parallel 可以组合使用,但顺序很重要:
- 先用
@memory.cache装饰你的单个处理函数(如process_item),让它自动缓存输入→输出映射 - 再用
Parallel调用一批带不同参数的该函数
这样,如果某次运行中部分输入之前算过,对应子任务会直接从磁盘加载结果,不进进程池、不占 CPU、不走序列化。
关键点:
- 缓存目录
location必须是可写的本地路径(不能是 NFS 或网络盘,否则并发写会冲突) - 如果函数参数里有
numpy数组,joblib默认按内容哈希(安全但略慢);加ignore=['arr']可跳过某参数的哈希(慎用,确保它不影响结果) - 缓存文件不会自动清理,长期运行需配
bytes_limit或定期手动删cachedir
容易被忽略的坑:Windows 下的 if __name__ == '__main__':
在 Windows 上,joblib 多进程依赖 spawn 方式启动新进程,这意味着:
- 所有并行相关代码(包括
Parallel调用)必须包裹在if <strong>name</strong> == '<strong>main</strong>':块内 - 否则子进程会重新执行整个脚本,导致无限递归创建进程、报
RecursionError或直接卡死
这不是建议,是硬性要求。哪怕你只是写个 demo 脚本,也得这么写:
from joblib import Parallel, delayed <p>def task(x): return x * 2</p><p>if <strong>name</strong> == '<strong>main</strong>': results = Parallel(n_jobs=2)(delayed(task)(i) for i in [1, 2, 3]) print(results)
Linux/macOS 虽然有时不报错,但为了一致性和可移植性,一律加上这个保护。
真正的难点不在语法,而在于判断:这个函数能不能拆?参数有没有隐式状态?缓存键是否稳定?并行收益是否被序列化/反序列化吃掉?这些没法靠文档,得看第一次跑出来的耗时分解。

















