目录
Joblib 进行并行操作:从入门到精通的超详细教程
一、引言:为什么需要并行计算?
在当今数据科学和机器学习领域,处理大规模数据集和复杂计算已成为常态。单线程执行的代码往往无法满足性能需求,而 Python 的 GIL(全局解释器锁)限制了多线程在 CPU 密集型任务中的效率。Joblib 作为 Python 中一个高效、易用的并行计算库,能够显著提升程序性能,特别适合数据科学和机器学习工作流。
二、Joblib 基础
(一)安装 Joblib
pip install joblib
(二)Joblib 核心概念
Joblib 提供了两个核心组件:
Parallel:用于并行执行任务delayed:用于包装函数,使其可以被并行调度
以下是Joblib核心组件Parallel与delayed的关键信息对比表格:
| 组件(Component) | 核心定位 | 核心功能 | 关键特性 |
|---|---|---|---|
| Parallel | 并行计算的“管理器” | 1. 管理并行资源(进程池/线程池) 2. 向工作单元分发任务 3. 收集并聚合所有任务结果 | 1. 支持with语句(上下文管理)2. n_jobs:控制并行任务数量(-1表示用全部核心)3. backend:选择并行模式(loky多进程/threading多线程)4. verbose:控制日志输出详细程度 |
| delayed | 任务延迟执行的“包装器” | 1. 封装目标函数及其传入的参数(位置参数+关键字参数) 2. 延迟函数执行(仅记录调用信息) 3. 生成可被 Parallel调度的任务对象 | 1. 本质返回(函数, args, kwargs)元组2. 不立即执行函数,仅为并行调度做准备 3. 需链式调用使用: delayed(目标函数)(参数1, 参数2, **kwargs) |
补充:两者协作关系
delayed负责“打包任务”,将函数与参数封装成可被调度的单元;Parallel负责“执行任务”,管理资源、分发打包好的任务并收集结果。二者需配合使用,典型范式为:
from joblib import Parallel, delayed
# delayed打包任务,Parallel执行任务
results = Parallel(n_jobs=-1)(delayed(func)(arg) for arg in args_list)
(三)基本用法
1. 基础语法
from joblib import Parallel, delayed
# 定义要并行执行的函数
def process_item(item):
# 处理单个任务的逻辑
return item * 2
# 生成任务列表
items = [1, 2, 3, 4, 5]
# 并行执行任务
results = Parallel(n_jobs=4)(delayed(process_item)(item) for item in items)
2. 详细工作流程
- 定义任务函数:创建一个处理单个任务的函数(如
process_item) - 生成任务队列:使用
delayed包装函数和参数,生成任务队列tasks = [delayed(process_item)(item) for item in items] - 执行并行任务:将任务队列传递给
Parallel对象results = Parallel(n_jobs=4)(tasks)
示例代码:
from joblib import Parallel, delayed
import time
def square(x):
"""计算平方(模拟耗时操作)"""
time.sleep(0.5) # 模拟耗时计算
return x * x
# 创建任务列表
numbers = list(range(10))
# 串行执行(用于对比)
start_time = time.time()
results_serial = [square(x) for x in numbers]
print(f"串行执行耗时: {time.time() - start_time:.2f} 秒")
# 并行执行
start_time = time.time()
results_parallel = Parallel(n_jobs=4)(delayed(square)(x) for x in numbers)
print(f"并行执行耗时: {time.time() - start_time:.2f} 秒")
输出示例:
串行执行耗时: 5.02 秒
并行执行耗时: 1.26 秒
(四)关键参数详解
| 参数 | 说明 | 默认值 | 常用值 |
|---|---|---|---|
n_jobs | 并行作业数 | 1 | -1(全部CPU核心) -2(全部核心-1) 4(指定数量) |
backend | 并行后端 | ‘loky’ | ‘threading’(多线程) ‘multiprocessing’(多进程) |
verbose | 日志详细程度 | 0 | 0(无) 50(详细) 100(非常详细) |
prefer | 优先后端 | None | ‘processes’(优先多进程) ‘threads’(优先多线程) |
require | 后端要求 | None | ‘sharedmem’(共享内存) |
1. n_jobs:并行任务数量
| 值 | 说明 |
|---|---|
n_jobs=1 | 串行执行(默认) |
n_jobs=4 | 使用4个并行任务 |
n_jobs=-1 | 使用所有可用CPU核心 |
n_jobs=-2 | 使用所有CPU核心减1 |
n_jobs=0 | 使用1个CPU核心(与n_jobs=1相同) |
最佳实践:
- CPU密集型任务:
n_jobs应设置为CPU核心数(如8核CPU用n_jobs=8) - I/O密集型任务:可以设置为更高值(如
n_jobs=16)
2. backend:并行后端选择
| 值 | 说明 | 适用场景 |
|---|---|---|
"loky" | 默认后端,基于多进程 | CPU密集型任务(数值计算、机器学习) |
"threading" | 基于多线程 | I/O密集型任务(网络请求、文件读写) |
"multiprocessing" | 旧版多进程 | 不推荐使用(loky更优) |
为什么默认使用"loky":
- Python的GIL限制了多线程在CPU密集型任务中的效率
- 多进程可以充分利用多核CPU
- 适合数据科学和机器学习中常见的CPU密集型任务
3. verbose:日志详细程度
| 值 | 说明 |
|---|---|
0 | 静默模式(默认) |
50 | 显示任务进度 |
100 | 显示详细执行信息 |
示例:
results = Parallel(n_jobs=4, verbose=50)(delayed(process_item)(item) for item in items)
4. pre_dispatch:任务预分发策略
| 值 | 说明 | 默认值 |
|---|---|---|
pre_dispatch="2*n_jobs" | 预分发2倍于n_jobs的任务 | 通常推荐使用 |
pre_dispatch="3*n_jobs" | 预分发3倍于n_jobs的任务 | 适合大数据集 |
pre_dispatch="10" | 预分发10个任务 | 适合小任务 |
为什么需要pre_dispatch:
- 控制初始分发的任务数量
- 避免一次性分发太多任务导致内存压力
- 优化任务调度效率
5. batch_size:批处理大小
| 值 | 说明 |
|---|---|
batch_size='auto' | 自动选择批处理大小(默认) |
batch_size=10 | 每个批次处理10个元素 |
batch_size=1 | 每个任务单独处理 |
适用场景:
- 处理大数据集时,避免内存溢出
- 任务粒度较大时,减少任务调度开销
三、并行计算的核心原理
(一)Joblib 的底层原理
Joblib 通过 loky 后端(默认)实现并行计算,其核心机制如下:
- 任务分发:
Parallel将任务列表分发给多个工作进程/线程 - 进程/线程创建:根据
n_jobs和backend创建相应数量的进程/线程 - 任务执行:每个工作单元执行
delayed包装的函数 - 结果收集:将所有结果收集并返回
(二)多进程 vs 多线程
| 特性 | 多进程 | 多线程 |
|---|---|---|
| GIL 影响 | 无(绕过 GIL) | 有(受 GIL 限制) |
| 适用任务 | CPU 密集型(数值计算、机器学习) | I/O 密集型(网络请求、文件读写) |
| 内存使用 | 高(每个进程独立内存) | 低(共享内存) |
| 崩溃影响 | 一个进程崩溃不影响其他 | 一个线程崩溃可能拖垮整个进程 |
| 默认后端 | ‘loky’ | ‘threading’ |
(三)为什么默认使用多进程?
Joblib 默认使用 loky 后端(基于多进程),因为:
- Python 的 GIL 限制了多线程在 CPU 密集型任务中的效率
- 多进程可以充分利用多核 CPU
- 适合数据科学和机器学习中常见的 CPU 密集型任务
四、高级用法
(一) 多参数处理
1. 两个参数都并行
def multiply(x, y):
return x * y
# 两个参数都并行
results = Parallel(n_jobs=4)(
delayed(multiply)(x, y) for x, y in zip(range(5), range(5, 10))
)
2. 部分参数并行
def multiply_with_constant(x, y, constant=2):
return x * y * constant
# 只有部分参数并行
results = Parallel(n_jobs=4)(
delayed(multiply_with_constant)(x, y, 3) for x, y in zip(range(5), range(5, 10))
)
(二)批处理(Batch)
当处理大量数据时,批处理可以有效管理内存:
from joblib import Batch
def process_batch(batch):
"""处理一个数据批次"""
return [x * 2 for x in batch]
numbers = list(range(100))
# 每个批次处理10个元素
results = Parallel(n_jobs=4)(
Batch(delayed(process_batch)(numbers[i:i+10]), size=10)
for i in range(0, len(numbers), 10)
)
# 合并结果
all_results = [item for sublist in results for item in sublist]
(三)自定义并行后端
通过parallel_backend上下文管理器动态切换后端:
from joblib import parallel_backend
# 使用多线程
with parallel_backend('threading'):
results = Parallel(n_jobs=4)(
delayed(square)(x) for x in range(10)
)
# 使用多进程
with parallel_backend('loky'):
results = Parallel(n_jobs=4)(
delayed(square)(x) for x in range(10)
)
(四)异常处理
在并行任务中正确处理异常:
def safe_square(x):
try:
return x * x
except Exception as e:
return f"Error: {str(e)}"
results = Parallel(n_jobs=4, verbose=1)(
delayed(safe_square)(x) for x in range(-5, 5)
)
print(results)
五、实际应用场景
(一)加速 Pandas 数据处理
import pandas as pd
from joblib import Parallel, delayed
# 创建一个示例数据集
df = pd.DataFrame({
'A': range(10000),
'B': range(10000, 20000)
})
def process_row(row):
"""处理单行数据"""
return row['A'] + row['B']
# 串行处理
start_time = time.time()
df['result_serial'] = df.apply(process_row, axis=1)
print(f"串行处理耗时: {time.time() - start_time:.2f} 秒")
# 并行处理
start_time = time.time()
results = Parallel(n_jobs=4)(delayed(process_row)(row) for _, row in df.iterrows())
df['result_parallel'] = results
print(f"并行处理耗时: {time.time() - start_time:.2f} 秒")
(二)网络请求并行化
import requests
from joblib import Parallel, delayed
def fetch_url(url):
"""获取URL内容"""
try:
response = requests.get(url, timeout=5)
return (url, response.status_code, len(response.text))
except Exception as e:
return (url, str(e), 0)
urls = [
"https://www.google.com",
"https://www.github.com",
"https://www.python.org",
"https://www.stackoverflow.com",
"https://www.wikipedia.org"
] * 10 # 创建10个重复URL
# 使用多线程处理网络请求(I/O密集型任务)
start_time = time.time()
results = Parallel(n_jobs=8, backend='threading')(
delayed(fetch_url)(url) for url in urls
)
print(f"并行网络请求耗时: {time.time() - start_time:.2f} 秒")
# 处理结果
for url, status, length in results:
print(f"{url}: {status} ({length} bytes)")
(三)机器学习中的并行应用
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
from sklearn.datasets import make_classification
from joblib import Parallel, delayed
# 创建示例数据
X, y = make_classification(n_samples=10000, n_features=20, random_state=42)
def train_model(n_estimators):
"""训练单个随机森林模型"""
model = RandomForestClassifier(n_estimators=n_estimators, n_jobs=1)
score = cross_val_score(model, X, y, cv=3, scoring='accuracy')
return (n_estimators, score.mean())
# 为不同参数组合训练模型
n_estimators_list = [50, 100, 150, 200]
# 串行执行
start_time = time.time()
results_serial = [train_model(ne) for ne in n_estimators_list]
print(f"串行训练耗时: {time.time() - start_time:.2f} 秒")
# 并行执行
start_time = time.time()
results_parallel = Parallel(n_jobs=4)(
delayed(train_model)(ne) for ne in n_estimators_list
)
print(f"并行训练耗时: {time.time() - start_time:.2f} 秒")
六、常见问题与解决方案
(一)多进程导致内存占用过高
原因:每个进程都有独立的内存空间
解决方案:
- 限制
n_jobs为CPU核心数 - 使用
batch_size控制每个任务的处理量 - 使用
Memory缓存结果,避免重复计算
(二)多线程在 CPU 密集型任务中无明显加速
原因:Python 的 GIL 限制了多线程在 CPU 密集型任务中的效率
解决方案:改用多进程(backend='loky' 或 backend='multiprocessing')
(三)使用Parallel时遇到AttributeError: Can't get attribute on错误
原因:多进程与__main__模块相关
解决方案:
- 将主要代码放入
if __name__ == '__main__':块中 - 将函数定义在模块级别(不在类中)
def my_function(x):
return x * x
if __name__ == '__main__':
results = Parallel(n_jobs=4)(delayed(my_function)(x) for x in range(10))
(四)并行执行速度比串行还慢
原因:
- 任务太小(任务调度开销大于计算时间)
n_jobs设置不当- I/O密集型任务使用了多进程
解决方案:
- 增加任务粒度(每个任务至少100ms计算时间)
- 合理设置
n_jobs(CPU密集型任务设为核心数,I/O密集型任务设得更大) - I/O密集型任务使用多线程(
backend='threading')
七、性能优化技巧
(一)内存优化技巧
-
合理设置
n_jobs:- 对于 CPU 密集型任务,
n_jobs通常设为 CPU 核心数 - 对于 I/O 密集型任务,
n_jobs可以设得更大
- 对于 CPU 密集型任务,
-
使用
pre_dispatch参数:- 控制初始分发的任务数量
- 避免一次性分发太多任务导致内存压力
# 例如,设置 pre_dispatch 为 3 * n_jobs
results = Parallel(n_jobs=4, pre_dispatch='3*n_jobs')(
delayed(square)(x) for x in range(100)
)
- 使用
batch_size:- 控制每个任务批次的大小
- 适用于非常大的数据集
results = Parallel(n_jobs=4, batch_size=10)(
delayed(square)(x) for x in range(100)
)
(二)性能调优技巧
- 避免全局变量:并行任务不应依赖全局变量,避免数据竞争
- 任务粒度:每个任务不应太小(避免任务调度开销)
- 结果收集:避免在并行任务中进行耗时的集合操作
(三)进度条显示(结合 tqdm)
使用 tqdm 显示进度条,实时了解任务进度:
from tqdm.auto import tqdm
from joblib import Parallel
class ProgressParallel(Parallel):
def __init__(self, use_tqdm=True, total=None, *args, **kwargs):
self._use_tqdm = use_tqdm
self._total = total
super().__init__(*args, **kwargs)
def __call__(self, *args, **kwargs):
with tqdm(disable=not self._use_tqdm, total=self._total) as self._pbar:
return super(ProgressParallel, self).__call__(*args, **kwargs)
def print_progress(self):
if self._total is None:
self._pbar.total = self.n_dispatched_tasks
self._pbar.n = self.n_completed_tasks
self._pbar.refresh()
# 使用示例
results = ProgressParallel(n_jobs=4, total=100)(
delayed(square)(i) for i in range(100)
)
八、总结
(一)Parallel与delayed的核心关系
- delayed:将函数调用延迟,生成任务队列
- Parallel:调度任务队列,执行并行计算
(二)选择正确后端的决策树
是否CPU密集型任务?
├─ 是 → 使用多进程(backend="loky")
└─ 否 → 使用多线程(backend="threading")
(三)最佳实践
- 任务类型匹配:
- CPU密集型任务 → 多进程(
backend="loky") - I/O密集型任务 → 多线程(
backend="threading")
- CPU密集型任务 → 多进程(
- 参数设置:
n_jobs=-1:使用所有CPU核心verbose=1:显示任务进度pre_dispatch='3*n_jobs':优化任务分发
- 性能优化:
- 任务粒度适中(每个任务至少100ms计算时间)
- 使用批处理(
Batch)处理大数据集 - 结合
tqdm显示进度条
- 错误处理:
- 在并行任务中添加异常处理
- 使用
verbose参数调试
- 内存管理:
- 大数据集使用批处理
- 避免在并行任务中创建大型对象
九、附录:Joblib 与类似库的比较
| 特性 | Joblib | Dask | concurrent.futures |
|---|---|---|---|
| 主要用途 | 简单并行计算 | 大规模分布式计算 | 基础并行API |
| 易用性 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
| 并行模式 | 多进程/多线程 | 多进程/分布式 | 多进程/多线程 |
| 内存管理 | 内置缓存 | 高级内存管理 | 无 |
| 适合场景 | 小到中型并行任务 | 大型分布式计算 | 简单并行任务 |
3337

被折叠的 条评论
为什么被折叠?



