4. 并行计算工具箱入门

MATLAB 的并行计算能力并不是“自动生效”的:默认情况下,for 循环仍然在单个工作进程(worker)上顺序执行。要让代码真正并行,需要先建立一个并行池(parallel pool),再使用能够把循环体分发到多个 worker 上的并行语言结构,最常用的就是 parfor。本章围绕并行池的创建与关闭、parfor 的基本语法与限制、以及任务如何被调度到 worker 上展开,目标是让你能独立写出第一个可运行、可验证的并行循环。

一、并行池是什么:worker、parpool 与客户端

并行池由一组独立的 MATLAB 工作进程组成,这些进程称为 worker。你当前操作的 MATLAB 桌面称为客户端(client)。客户端负责提交任务、收集结果;worker 负责实际计算。默认的本地并行池使用本机 CPU 的多个核心,每个 worker 对应一个计算进程,因此并行加速的上限通常受物理核心数限制。

并行池不是越多越好。worker 数量超过物理核心数时,多个 worker 会争抢同一核心,反而增加上下文切换开销。一般建议 parpool 的 worker 数不超过本机物理核心数;对于内存占用大的任务,还要考虑每个 worker 复制一份数据带来的内存压力。

并行池结构:客户端提交任务,worker 并行执行 客户端 (Client) MATLAB 桌面 parpool 创建池 parfor 提交循环 收集结果 汇总到变量 delete(pool) 关闭池 任务分发 结果回传 并行池 (Parallel Pool) Worker 1 迭代 i=1,5,9… 独立内存空间 Worker 2 迭代 i=2,6,10… 独立内存空间 Worker N 迭代 i=N,2N… 独立内存空间 任务调度器 (Scheduler) 将 parfor 的迭代区间切分并分配给空闲 worker 动态负载均衡:先完成的 worker 领取新任务

二、启动与关闭并行池

最常用的启动方式是 parpool。不带参数时,MATLAB 会按默认配置启动本地池,worker 数通常等于可用核心数。也可以显式指定 worker 数量:

% 启动 4 个 worker 的本地并行池
pool = parpool(4);

% 查看当前池信息
disp(pool.NumWorkers)   % 输出 4
disp(pool.Cluster)      % 输出 local

% 使用完毕后关闭
delete(pool);

如果池已经存在,再次调用 parpool 会直接返回现有池,不会重复创建。可以用 gcp(get current pool)获取当前池句柄。关闭池使用 delete(pool);若想关闭当前池而不持有句柄,可用 delete(gcp('nocreate'))。注意 gcp('nocreate') 在无池时返回空,不会自动创建池。

并行池的启动需要几秒到几十秒,取决于 worker 数量和启动方式。如果一段代码要反复运行,建议在脚本开头创建一次池,在结尾统一关闭,而不是每次循环都创建和销毁。

三、parfor 基本语法与适用条件

parfor 的写法与 for 几乎相同,只是把关键字替换掉:

n = 100;
results = zeros(1, n);

parfor i = 1:n
    results(i) = sqrt(i) * sin(i);
end

这段代码把 1 到 100 的迭代分配给多个 worker。每个 worker 计算一部分 i,把结果写回 results 的对应位置。关键限制是:循环迭代之间必须相互独立。也就是说,第 i 次迭代不能依赖第 i-1 次迭代的结果,也不能在迭代中修改会被其他迭代读取的共享变量。

常见的违规写法包括:

  • 累加式依赖:s = s + x(i),其中 s 在循环外初始化并被所有迭代共享。
  • 索引依赖:a(i) = a(i-1) + 1,后一次迭代读取前一次写入的值。
  • 动态增长:在循环内用 a(end+1) = ... 扩展数组,不同 worker 无法安全协调。

如果确实需要归约操作(求和、求最大值等),应使用 MATLAB 支持的归约变量形式,例如 total = total + x(i) 在 parfor 中是被允许的,因为每个 worker 会维护局部副本,最后再合并。但更推荐用向量化或 sum、max 等函数直接处理,避免依赖隐式归约。

四、任务调度初步:迭代如何被分配

当 parfor 开始执行时,MATLAB 会把迭代区间切分成若干块(chunk),分发给池中的 worker。默认策略是动态调度:先给每个 worker 分配一块,谁先完成谁再领取下一块。这样能缓解不同迭代耗时不一致带来的负载不均。

调度粒度会影响性能。块太小,任务分发和结果回收的通信开销占比升高;块太大,负载均衡效果变差。MATLAB 会根据迭代次数和 worker 数量自动选择,一般不需要手动干预。但在迭代耗时差异极大时,可以通过调整循环结构或使用 parfor 的选项来改善。

并行并不总是更快。如果单次迭代的计算量很小(例如只是几次简单算术),而迭代次数又不多,那么任务分发、数据序列化和结果回传的开销可能超过并行带来的收益。此时顺序 for 反而更快。判断依据是:单次迭代计算量是否足够大、迭代之间是否独立、数据量是否适合分发。

五、一个可验证的完整示例

下面用一个计算量适中的例子观察并行效果。任务是对每个 i 计算一个较大矩阵的特征值之和,单次迭代耗时约几毫秒到几十毫秒,适合并行。

n = 200;
vals = zeros(1, n);

% 启动池(假设本机 4 核)
pool = parpool(4);

tic;
parfor i = 1:n
    A = rand(300) + i * eye(300);   % 构造一个随 i 变化的矩阵
    vals(i) = sum(eig(A));          % 计算特征值之和
end
t_par = toc;

delete(pool);

% 顺序版本对比
tic;
for i = 1:n
    A = rand(300) + i * eye(300);
    vals_seq(i) = sum(eig(A));
end
t_seq = toc;

fprintf('并行耗时: %.2f s\n', t_par);
fprintf('顺序耗时: %.2f s\n', t_seq);
fprintf('加速比: %.2f\n', t_seq / t_par);

运行后应观察三点:第一,vals 与 vals_seq 是否一致(在浮点误差范围内);第二,并行耗时是否明显小于顺序耗时;第三,加速比是否接近 worker 数量。如果加速比远低于预期,常见原因是迭代计算量太小、数据分发开销大,或者 worker 数量超过了物理核心数。

六、常见配置与排错要点

现象可能原因处理方式
parpool 启动失败许可证不含并行工具箱,或 worker 数超过限制用 ver 确认 Parallel Computing Toolbox 已安装;减少 worker 数
parfor 报“变量无法分类”循环内使用了不支持的变量类型或索引方式检查是否有跨迭代依赖、动态增长或嵌套 parfor
并行比顺序还慢单次迭代计算量过小,通信开销占主导增大单次迭代计算量,或改回顺序 for
内存不足每个 worker 复制一份大数组减少 worker 数,或改用分布式数组、datastore