Skip to content

写在前面

如果你搜过"并行计算"、"超算"、"分布式仿真"这些词,几乎一定会碰到 MPI 三个字母。它听起来很吓人,像是只有国家实验室才会用的东西。

其实没有。这篇博客从零开始,讲清楚 MPI 是什么、为什么需要它、最小可用的代码长什么样。看完你应该能独立写一个跑在多进程上的小程序 ✨。


一、MPI 是什么

MPI 全称 Message Passing Interface(消息传递接口)。它不是一门语言,也不是某种具体的软件,而是一套规范

规定好"进程之间怎么发消息",然后大家照着这个规范去实现。

当前最主流的两个开源实现:

实现说明
OpenMPI开源、社区版,几乎所有 Linux 发行版都能直接装
MPICH另一套主流开源实现,Intel MPI、Microsoft MPI 都基于它

MPI 不是 C++ 专属

MPI 是语言无关的标准。C/C++ 是最常用的,但同样支持 Fortran、Python(mpi4py)、Java、Rust 等。


二、为什么需要 MPI

先想一个问题:你的 CPU 是 8 核的,但跑一个普通程序的时候,任务管理器里只有一个核心跑满 100%,其他 7 个核心大多时候是空闲的

为什么?因为默认程序只有一个进程,所有活都压在一个核心上

要让 CPU 火力全开,有两条路:

路线思路适用场景
多线程(OpenMP、std::thread)一个进程,多个线程,共享内存单机内多核
多进程(MPI)多个进程,各自独立内存,靠"消息"通信跨机器、可扩展到上千节点

MPI 的核心场景:任务太大,一台机器装不下——比如气象预报、分子动力学、深度学习训练。这些都需要把几十台、几百台服务器的算力拼起来。


三、几个核心概念

MPI 的世界由几个关键名词组成,先把它们记牢:

3.1 进程(Process)

MPI 程序启动后会生成 N 个进程,每个进程都跑同一份代码。

text
你写了一份 code.c,N=4,启动:
    进程 0   ──┐
    进程 1   ──┤── 都跑 code.c 里的 main(),但各自有独立内存
    进程 2   ──┤
    进程 3   ──┘

3.2 Rank(编号)

每个进程有一个独一无二的编号,叫 rank,从 0 开始。

进程 0 通常被当作"主进程",做一些分发、汇总的工作。

3.3 通信域(Communicator)

通信域是一组进程的"群聊房间"。默认的房间叫 MPI_COMM_WORLD,里面包含所有进程。

你也可以自定义通信域,把进程分成几个小组,分别做不同的事。

3.4 点对点通信 vs 集体通信

  • 点对点通信:进程 A 发消息给进程 B
  • 集体通信:一个组的所有进程一起参与(比如"全员广播"、"全员求和")

四、环境准备

4.1 Linux(推荐)

bash
sudo apt install openmpi-bin libopenmpi-dev
bash
sudo yum install openmpi openmpi-devel
bash
sudo pacman -S openmpi

装完确认一下:

bash
which mpicxx    # C++ 编译器包装
which mpirun    # 启动 MPI 程序的命令

4.2 macOS

bash
brew install open-mpi

4.3 Windows

Windows 上推荐用 WSL2(装个 Ubuntu 子系统),按 Linux 步骤装。原生 Windows 可以装 MS-MPI,但体验略差。


五、第一个程序:Hello MPI

5.1 编写代码

新建 hello.cpp

cpp
#include <mpi.h>
#include <iostream>

int main(int argc, char** argv) {
    // 1. 初始化 MPI 环境
    MPI_Init(&argc, &argv);

    // 2. 获取当前进程编号
    int world_rank;
    MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);

    // 3. 获取总进程数
    int world_size;
    MPI_Comm_size(MPI_COMM_WORLD, &world_size);

    std::cout << "Hello from process " << world_rank
              << " of " << world_size << std::endl;

    // 4. 结束 MPI 环境
    MPI_Finalize();
    return 0;
}

每个 MPI 程序都长这个结构:

步骤函数作用
初始化MPI_Init启动 MPI 环境
干正事...你的并行逻辑
收尾MPI_Finalize关闭 MPI 环境

5.2 编译 & 运行

bash
# 用 mpicxx 替代 g++/clang++
mpicxx hello.cpp -o hello
bash
mpirun -np 4 ./hello

-np 是什么

-np 就是 number of processes(进程数)。你写 1,它就当普通程序跑;写 4,输出就是 4 行。

输出:

text
Hello from process 0 of 4
Hello from process 1 of 4
Hello from process 2 of 4
Hello from process 3 of 4

注意:输出的顺序不固定。多个进程的 std::cout 在抢 stdout,谁先刷出来谁先打印。


六、点对点通信:Send / Recv

进程之间最基础的通信方式:一个发,一个收

cpp
#include <mpi.h>
#include <iostream>

int main(int argc, char** argv) {
    MPI_Init(&argc, &argv);

    int rank;
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);

    int number;
    if (rank == 0) {
        number = 100;
        // 把 number 发给进程 1
        MPI_Send(&number, 1, MPI_INT, 1, 0, MPI_COMM_WORLD);
        std::cout << "Process 0 sent " << number << " to process 1\n";
    } else if (rank == 1) {
        // 从进程 0 接收一个 int
        MPI_Recv(&number, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
        std::cout << "Process 1 received " << number << " from process 0\n";
    }

    MPI_Finalize();
    return 0;
}

MPI_SendMPI_Recv 的参数虽然多,但套路是固定的:

text
MPI_Send(数据地址, 个数, 类型, 目标rank, 标签, 通信域)
MPI_Recv(数据地址, 个数, 类型, 来源rank, 标签, 通信域, 状态)

Send 和 Recv 必须配对

如果进程 0 在发,但进程 1 没在收(或者反之),程序就会卡住。MPI 的点对点通信是同步的,没人接就不走。


七、集体通信:让一组进程一起干活

实际项目里,点对点通信写起来太啰嗦。集体通信才是主力。

7.1 Broadcast(广播)

"一个人说,所有人听"。比如主进程把配置参数广播给所有子进程:

cpp
int data;
if (rank == 0) {
    data = 42;
}
// 所有进程都调用 MPI_Bcast
// 进程 0 发出 data,其他进程接收 data
MPI_Bcast(&data, 1, MPI_INT, 0, MPI_COMM_WORLD);
// 现在每个进程的 data 都是 42

7.2 Reduce(归约)

"所有人都交一份数据上来,主进程汇总"。

最常用的场景:并行求和

cpp
int local_sum = rank + 1;  // 每个进程贡献自己的数
int global_sum = 0;

// 所有进程都调用 MPI_Reduce
// 结果只保留在 root=0 进程,其他进程 global_sum 是未定义值
MPI_Reduce(&local_sum, &global_sum, 1, MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD);

if (rank == 0) {
    std::cout << "1 + 2 + 3 + 4 = " << global_sum << std::endl;  // 输出 10
}

MPI_SUM 是归约操作符,常见的还有:

操作符含义
MPI_SUM求和
MPI_MAX最大值
MPI_MIN最小值
MPI_PROD求积

7.3 Scatter & Gather(分发与收集)

  • Scatter:主进程把一个数组切片分给各个进程
  • Gather:各个进程把自己的数据汇总到主进程
cpp
// Scatter 示例
int sendbuf[4] = {10, 20, 30, 40};  // 主进程的数据
int recvbuf;                         // 每个进程接收一个数

if (rank == 0) {
    MPI_Scatter(sendbuf, 1, MPI_INT, &recvbuf, 1, MPI_INT, 0, MPI_COMM_WORLD);
}
// 现在 rank=0 拿到 10,rank=1 拿到 20,rank=2 拿到 30,rank=3 拿到 40

7.4 Barrier(同步栅栏)

"所有人到这里集合,凑齐了再一起走"。

cpp
std::cout << "Process " << rank << " before barrier\n";
MPI_Barrier(MPI_COMM_WORLD);
std::cout << "Process " << rank << " after barrier\n";

效果:所有 "after barrier" 一定在所有 "before barrier" 之后才打印


八、一个完整例子:并行计算 π

把上面所有概念串起来,做点有意义的事。

思路:用积分公式求 π:

$$\pi = \int_0^1 \frac{4}{1+x^2} dx$$

把区间 [0, 1] 切成 N 份,每个进程算其中一份的面积,最后求和。

cpp
#include <mpi.h>
#include <iostream>
#include <cmath>

int main(int argc, char** argv) {
    MPI_Init(&argc, &argv);

    int rank, size;
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Comm_size(MPI_COMM_WORLD, &size);

    const long long N = 100000000;  // 总切分数
    long long local_n = N / size;    // 每个进程算多少份
    long long start = rank * local_n;
    long long end = start + local_n;

    double local_sum = 0.0;
    double dx = 1.0 / N;
    for (long long i = start; i < end; i++) {
        double x = (i + 0.5) * dx;
        local_sum += 4.0 / (1.0 + x * x);
    }
    local_sum *= dx;

    // 把所有进程的 local_sum 加起来,结果给 rank=0
    double global_sum = 0.0;
    MPI_Reduce(&local_sum, &global_sum, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);

    if (rank == 0) {
        double pi = global_sum;
        std::cout << "π ≈ " << pi << std::endl;
        std::cout << "真实值: " << M_PI << std::endl;
        std::cout << "误差:  " << std::abs(pi - M_PI) << std::endl;
    }

    MPI_Finalize();
    return 0;
}

编译运行:

bash
mpicxx pi.cpp -o pi
mpirun -np 4 ./pi

输出类似:

text
π ≈ 3.14159265358979
真实值: 3.14159265358979
误差:  2.67e-16

从 0 到 1 亿次的循环,4 个进程一起算,比单进程快得多。这就是 MPI 的核心价值 🎯。


九、常见误区

9.1 以为 MPI 会自动加速

。MPI 只是一种通信方式。你的算法必须能被切分成独立子任务,否则越多的进程反而越慢(通信开销会盖过计算收益)。

9.2 每个进程都能独立访问全部数据

。MPI 是分布式内存,每个进程的变量是私有的。想共享数据,必须显式发消息

9.3 进程越多越快

⚠️ 不一定。随着进程数增加,通信开销也会增加。存在一个"甜点",超过之后性能反而下降。这叫 Amdahl 定律

9.4 MPI_Init 之前的输出会丢

⚠️ 在某些实现下,MPI_Init 之前调用 std::cout 是不安全的。所有 MPI 调用前必须先 Init


十、学习路线建议

按这个顺序往下走最稳:

  1. 本篇:MPI 是什么、Send/Recv、集体通信
  2. 📖 MPI 数据类型:自定义结构体怎么发
  3. 📖 MPI 通信模式MPI_SsendMPI_Bsend 等的区别
  4. 📖 进程组与拓扑MPI_Cart_create 等高级用法
  5. 📖 混合并行:MPI + OpenMP(进程内再用线程)
  6. 📖 实战项目:用 MPI 重写一个你熟悉的算法(比如矩阵乘法)

推荐资料:

  • 📘 MPI 官方标准文档(mpich.org)
  • 📘 LLNL MPI Tutorial——劳伦斯利弗莫尔国家实验室的入门教程,写得非常友好
  • 📘 《MPI 并行程序设计实例教程》——经典中文教材

写在最后

MPI 看起来复杂,本质上就两件事:

  1. 把任务切成 N 份,每份给一个进程
  2. 进程之间用消息拼出最终结果

掌握 SendRecvBcastReduce 这四个函数,你就能解决 80% 的入门问题。再往上就是熟练度和工程经验的事了。

Happy hacking 🚀