MPI并行编程——多进程程序设计
MPI(Massage Passing Interface)不是一种语言,是消息传递函数库的标准规范。MPI标准定义了一组具有可移植性的编程接口,在Fortran和C/C++中可以直接对相应的函数进行调用。本文对MPI消息的发送和接收作简要介绍。并且以PI的计算(使用C/C++编写)等几个程序作为示例。
MPI(Massage Passing Interface),它不是一种语言,而是一种库描述,是消息传递函数库的标准规范。MPI标准定义了一组具有可移植性的编程接口,在Fortran和C/C++中可以直接对相应的函数进行调用。
MPI有很多种实现。MPICH是最重要的MPI实现之一,它与其衍生产品构成了世界上使用最广泛的MPI实现,在超级计算机中也得到了广泛的应用。
MPI最基本的消息传递操作包括:发送消息send、接受消息receive、进程同步barrier、归约reduction等。本文对发送消息和接受消息作简要的介绍,如要了解更多MPI的相关知识,请参考其他相关资料。
一、MPI基本概念与相关的函数解释
通信器(communicator)
所谓通信器,可以理解为一类进程的集合,也可以称之为一个进程组。一个进程组中的进程可以相互通信。所有MPI通信都必须在某个通信器内进行。MPI系统提供缺省的通信器MPI_COMM_WORLD,所有启动的MPI进程通过调用函数MPI_Init()包含在该通信器内。各个进程可以通过函数MPI_Comm_size()获取通信器所包含的的MPI进程个数。一个通信器内的所有进程都拥有一个该通信器内中唯一的序号(rank)用作自身的标识( 序号的取值范围是[0,通信器进程数-1] )
int MPI_Comm_size(MPI_Comm comm, int* size)
//获取通信器的进程数,comm为通信器名称(也叫通信域),获取得到的进程个数结果被放在size中
进程序号(rank)
rank用来在一个通信器中标识一个进程,同一个进程在不同的通信器中可以有不同的序号,进程的序号是在通信器被创建时赋予的。
int MPI_Comm_rank(MPI_Comm comm, int* rank)
//获取进程在通信器中的标号,comm为通信器名称,获取得到的进程id被放在rank中
消息(message)
不同进程之间通过消息传递数据,因此MPI可以应用于分布存储系统。消息发送与接收函数的参数可以分为数据(data)和信封(envelope)两个部分。包装由进程序号、消息标号和通信器三部分组成;
int MPI_Send(void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)
//消息发送函数,参数可以分为数据、信封
//数据:<地址(buffer的地址),数据个数(count),数据类型(datatype)>
//信封:<目的(dest),标识(tag),通信域(comm)>
//count指定数据类型的个数、datatype为数据类型、dest取值范围是0~(进程总数-1);tag 取值范围是0~MPI_TAG_UB,用来区分消息
int MPI_Recv(void* buf, int count, MPI_Datatype datatype, int source,int tag, MPI_Comm comm, MPI_Status *status)
//接收buffer必须至少可以容纳count个由datatype参数指明类型的数据. 如果接收buf太小, 将导致溢出。
//消息匹配机制:
//1.参数匹配 dest,tag,comm 分别与 source,tag,comm相匹配;
//2.若 Source 为 MPI_ANY_SOURCE:接收任意处理器来的数据。
//3.若Tag为MPI_ANY_TAG:则匹配任意tag值的消息
//在阻塞式消息传送中不允许Source==Dest,否则会导致死锁。
//消息传送被限制在同一个通信域中。
//在send函数中必须指定唯一的接收者
int MPI_Get_count(MPI_Status status, MPI_Datatype datatype, int*count)
//查询接收到的消息长度,该函数在count中返回数据类型的个数,即消息的长度(count属于MPI_Status结构的一个域,但不能被用户直接访问)
需要注意:
发送进程需指定一个有效的目标接收进程
接收进程需指定一个有效的源发送进程
接收和发送消息的进程要在同一个通信器内
接收和发送消息的tag要相同
接收缓存区要足够大
二、MPI环境配置
2.1 gcc/g++环境配置
Ubuntu默认没有安装gcc/g++等编译环境。为简化环境配置,可以使用build-essential软件包。该软件包内包含了gcc/g++/gfortran等编译器.
sudo apt install build-essential
2.2 mpich环境配置
可直接使用如下命令安装mpich。
sudo apt install mpich
2.3 版本信息查看
通过在命令行输入以下信息查看程序版本
gcc版本查看(C语言编译器)
gcc -v
g++版本查看(C++编译器)
g++ -v
mpicc版本查看(使用C语言编写的MPI程序,需要使用mpicc进行编译)
mpicc -v
mpic++版本查看(使用C++编写的MPI程序,需要使用mpic++进行编译)
mpic++ -v
三、MIP的编译与运行
3.1程序的编译
使用以下命令可以编译mpi程序:
使用C语言编写的MPI程序:
mpicc -o XXX XXX.cpp
使用C++编写的MPI程序:
mpic++ -o XXX XXX.cpp
3.2 程序的运行
mpi程序的运行:
mpirun -np Y ./XXX
// Y表示的是一个数字,代表并行运行的进程数目。
// XXX表示源程序文件
3.3 HelloWorld示例程序
#include <iostream>
#include "mpi.h"
//使用了MPI程序需要包含mpi.h头文件
using namespace std;
int main(int argc, char** argv)
{
MPI_Init( &argc , &argv);
cout<<"Hello world!"<<endl;
MPI_Finalize();
return 0;
}
该程序只是使每个进程输出hello world!运行结果如下:
四、MPI数据类型(与C语言对应)
MPI 数据类型 | C语言数据类型 |
MPI_CHAR | char |
MPI_SHORT | short int |
MPI_INT | int |
MPI_LONG | long |
MPI_FLOAT | float |
MPI_DOUBLE | double |
MPI_LONG_DOUBLE | long double |
MPI_BYTE | |
MPI_PACKED |
五、MPI基本函数
int MPI_Init(int *argc, char **argv)
//初始化函数。MPI_INIT是MPI程序的第一个调用,完成MPI程序的所有初始化工作,启动MPI环境,标志并行代码的开始,因此要求main函数必须带参数运行
int MPI_Finalize(void)
//退出/结束函数。它是MPI程序的最后一个调用,结束MPI程序的运行,是MPI程序的最后一条可执行语句。它标志并行代码的结束,结束除主进程外其它进程
int MPI_Initialized(int* flag)
//允许在MPI_Init前使用的函数,检测MPI系统是否已经初始化
int MPI_Get_processor_name(char* name, int* resultlen)
//获取处理器的名称,在返回的name中存储所在处理器的名称,resultlen存放返回名字所占字节,应提供参数name不少于MPI_MAX_PRCESSOR_NAME个字节的存储空间
double MPI_Wtime(void)
//返回调用时刻的墙上时间,用浮点数表示秒数,墙上时钟时间wall clock time,也叫时钟时间,是指从进程从开始运行到结束,时钟走过的时间,这其中包含了进程在阻塞和等待状态的时间。用来计算程序运行时间。
六、几个简单示例程序
6.1 Print
输出每个进程的信息和运行时间
#include <iostream>
#include "mpi.h"
using namespace std;
int main(int argc, char** argv)
{
int pid, pnum,namelen;
double starttime;
char processor_name[MPI_MAX_PROCESSOR_NAME];
MPI_Init( &argc , &argv);
//初始化
starttime=MPI_Wtime();
//开始时间
MPI_Comm_size( MPI_COMM_WORLD , &pnum);
//获取通信域内进程个数
MPI_Comm_rank( MPI_COMM_WORLD , &pid);
//获取本进程的rank
MPI_Get_processor_name( processor_name , &namelen);
//获取processor_name
cout<<"this is "<<pid<<" of "<<pnum<<" and my name is "<< processor_name <<endl;
cout<<"this is "<<pid<<" and spend ";
printf("%.6lf s\n",MPI_Wtime()-starttime);
//输出信息
MPI_Finalize();
//结束并退出
return 0;
}
命令行:
mpic++ -o print.o print.cpp
mpirun -np 4 ./print.o
6.2 π的计算
#include <iostream>
#include "mpi.h"
using namespace std;
int main(int argc, char**argv)
{
//mpirun -np 4 calculatePI.o 800 其中的800是以参数的形式传入的,位于argv[1]
long double pi=0, answer=0, PI=3.141592653589793238462643383279;
int size, id, namelen,n=1000;
double time;
char processor_name[MPI_MAX_PROCESSOR_NAME];
MPI_Status status;
//开始计时
time=MPI_Wtime();
//如果参数列表中制定了n的值,则将该值赋给n
if(argc==2)sscanf(argv[1], "%d", &n);
MPI_Init(&argc, &argv);
//获取进程信息
MPI_Comm_size( MPI_COMM_WORLD , &size);
MPI_Comm_rank( MPI_COMM_WORLD , &id);
//比较n和size大小,若n过小,则返回
if(n<size)
{
cout<<"输入n值过小, 请重新输入"<<endl;
MPI_Finalize();
return 0;
}
for(int i=id; i<=n; i+=size)
{
long double tempans;
tempans = (long double)1/(long double)(2*i+1);
if(i%2==0)
{
answer+=tempans;
}
else
{
answer-=tempans;
}
}
//如果是主进程
if(id==0)
{
long double recvbuf;
pi=answer;
for(int i=1; i<size; i++)
{
MPI_Recv( &recvbuf , 1 , MPI_LONG_DOUBLE , MPI_ANY_SOURCE , 0 , MPI_COMM_WORLD , &status);
pi+=recvbuf;
}
}
else//发送消息并退出程序
{
MPI_Send( &answer , 1 , MPI_LONG_DOUBLE , 0 , 0 , MPI_COMM_WORLD);
MPI_Finalize();
return 0;
}
pi*=4;
cout<<"主进程使用"<<MPI_Wtime()-time<<"秒, 最后得到PI的计算结果为: ";
printf("%.20Lf\n",pi);
cout<<"n = "<<n<<" 使用了 "<<MPI_Wtime()-time <<"s pi = ";
printf("%.20Lf %.20Lf \n",pi,abs(PI-pi));
MPI_Finalize();
return 0;
}
命令行:
mpic++ -o calculatePI.o calculatePI.cpp
mpirun -np 1 calculatePI.o 200000000
mpirun -np 4 calculatePI.o 200000000
运行结果为:
如有不当或错误之处,恳请您的指正,谢谢!!!
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)