并行编程之MPI使用简析(1)
本文的实例代码来自于 Introduction to Parallel Programming (Peter Pacheco),并在对原文翻译的基础上归纳了一些个人观点。
MPI
首先我们要了解的是并行的MIMD(Multiple instructions multiple data)程序分两种:
分布式内存(distributed memory)并行程序
共享式内存(shared memory)并行程序
这两种程序的区别和实现方式很好理解。
举个例子,住在旧金山的Bob和住在上海的Alice要合作写一本书,他们之间相隔太远只能通过互相发邮件来同步写作进度。反观我们的并行程序,我们把Alice和Bob写书的工作看成两个进程,写邮件的方式就叫做消息传递。
第二个例子,Alice和Bob在同一个工作室内写书,他们喜欢在各自独立的房间内工作,用一块公共留言板来交流工作进度。工作室的例子是来说明Alice进程和Bob进程在同一台计算机上运行,公共留言板好比内存,两个人在统一内存空间读写方式就叫共享内存
MPI(Message Passing Interface消息传递接口)用于分布式内存编程,由于分布式内存程序可能运行在不同的机器上,进程与进程间的通信只能靠消息传递来实现。
在程序开头加上 #include <mpi.h>
就能使用MPI函数。
MPI函数名的特点是:以MPI_开头,紧跟其后的单词的首字母大写
下面的程序中用到的MPI函数有:
MPI_Init
MPI_Finalize
MPI_Send
MPI_Receive
下面来看一个消息收发的并行程序
看之前有一点需要声明:
并行程序是一个程序,也就是说,每个进程运行的程序都是由下列代码编译而来的。这种操作叫做SPMD(single program, multiple data)
#include <stdio.h>
#include <string.h> /∗ For strlen ∗/
#include <mpi.h> /∗ For MPI functions, etc ∗/
const int MAX_STRING = 100; /* 可接收的最大字符串长度 */
int main(void) {
char greeting[MAX_STRING];
int comm_sz; // 进程总数
int my_rank; // 进程编号
/* 初始化 */
MPI_Init(NULL, NULL);
MPI_Comm_size(MPI_COMM_WORLD, &comm_sz);
MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
if (my_rank != 0) {
/* 除主进程外的其他进程 */
sprintf(greeting, "Greetings from process %d of %d!", my_rank, comm_sz);
MPI_Send(greeting, strlen(greeting)+1, MPI_CHAR, 0, 0, MPI_COMM_WORLD);
}else{
/* 主进程(进程编号为0的进程) */
printf("Greetings from process %d of %d!\n", my_rank,
comm_sz);
/* 打印从其他进程收到的消息 */
for (int q = 1; q < comm_sz; q++) {
MPI_Recv(greeting, MAX_STRING, MPI_CHAR, q,
0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
printf("%s\n", greeting);
}
}
MPI_Finalize();
return 0;
}/∗ main ∗/
MPI_Init和MPI_Finalize
在使用其他的MPI函数前要用MPI_Init()进行初始化
int MPI_Init(
int* argc_p /* in/out */,
char** argv_p /* in/out */);
这里的argc_p
和argv_p
是指向main函数种argc, argv的指针,但是现在我们用不到,所以把两者都设为NULL
MPI_Finalize(void);
是用来告诉MPI系统:“嘿,我用完MPI了,分给MPI的资源现在可以释放了!”
概括来说,其他的MPI函数必须在MPI_Init
和MPI_Finalize
之间调用。
MPI_COMM_WORLD
在介绍MPI_Send
和MPI_Recieve
之前先要讲一下什么是Communicator也就是代码中出现的MPI_COMM_WORLD
MPI_COMM_WORLD
是MPI_Comm
类型的数据,它就像是一个聊天群。MPI_Init
的时候,聊天群的用户就是该程序所有并行的线程。
下面两个函数是用来获取Communicator的信息的:
/* comm_sz_p 得到线程总个数 */
int MPI_Comm_size(
MPI_Comm comm /* in */,
int* comm_sz_p /* out */);
/* my_rank_p 得到该线程的编号 */
int MPI_Comm_rank(
MPI_Comm comm /* in */,
int* my_rank_p /* out */);
MPI_Send和MPI_Recieve
这样MPI_Send
和MPI_Recieve
就很好理解了,它们相当于聊天群中点开某个人的头像开始私聊。
看一下MPI_Send的函数定义:
int MPI_Send(
void* msg_buf_p /* in */,
int msg_size /* in */,
MPI_Datatype msg_type /* in */,
int dest /* in */,
int tag /* in */,
MPI_Comm communicator /* in */);
前三个参数决定了消息的内容,后三个参数决定了发给谁。
/* 这里 strlen(greeting)+1 的1是greeting字符串最后的 '\0'
* msg_size必须小于等于buffer的大小,这里greeting可以放100个字符
* */
MPI_Send(greeting, strlen(greeting)+1, MPI_CHAR, 0, 0, MPI_COMM_WORLD);
msg_type可以为:
MPI_datatype | C datatype |
---|---|
MPI_CHAR | signed char |
MPI_SHORT | signed short int |
MPI_INT | signed int |
MPI_LONG | signed long int |
MPI_LONG_LONG | signed long long int |
MPI_UNSIGNED_CHAR | unsigned char |
MPI_UNSIGNED_SHORT | unsigned short int |
MPI_UNSIGNED | unsigned int |
MPI_UNSIGNED_LONG | unsigned long int |
MPI_FLOAT | float |
MPI_DOUBLE | double |
MPI_LONG_DOUBLE | long double |
MPI_BYTE | |
MPI_PACKED |
每个MPI函数都会包括communicator的参数,如果你的程序很复杂,有多个不同的communicator,MPI可以保证不同communicator中的进程不能相互通信。
tag参数的作用:一个process向另一个process发多个消息(消息的大小,类型还凑巧是一样的,就需要通过tag来配对)
看一下MPI_Recieve的函数定义
int MPI_Recv(
void* msg_buf_p /* out */,
int buf_size /* in */,
MPI_Datatype buf_type /* in */,
int source /* in */,
int tag /* in */,
MPI_Comm communicator /* in */,
MPI_Status* status_p /* out */);
前三个参数定义了接收数据的buffer,后三个参数是关于message的,status_p
暂时用不到,先定义为MPI_STATUS_IGNORE
Here comes the questions:
1) q进程的MPI_Send和r进程的MPI_Recieve是如何配对的呢?
答:
- recv_comm = send_comm ,
- recv_tag = send_ tag ,
- dest = r ,
- src = q .
单是这样还不够,
如果recv_type = send_type 并且 recv_buf_sz ≥ send_ buf_ sz , 那么q进程发送的message才有可能成功地被r进程收到。
2) 0号进程分配工作给num-1个进程干,num-1号进程先干完,却因0号进程是按进程号MPI_Receive结果的,所以num-1号进程得眼巴巴地等着,该怎么解决这种情况呢?
答:
dest定义为MPI_ANY_SOURCE
,下面的代码就能按活干完的先后顺序Recieve结果:
for (i = 1; i < comm_sz; i++) {
MPI_Recv(result, result_sz, result_type, MPI_ANY_SOURCE, result_tag, comm, MPI_STATUS_IGNORE);
Process result(result);
}
同理:tag 可以设为MPI_AND_TAG,但是这样Reciever就有可能不知道Sender的src和tag。别担心,status_p记录了包括src,tag和message大小的所有信息。
3) MPI_Send和MPI_Recieve是如何实现的?
答: MPI_Send
的实现方式有两种:
buffer
block
1 buffer: 设置缓冲区,即Send的时候将message的内容放入MPI内部的缓冲中
2 block: 阻塞直到message被Recieve,即Send的时候等待相应的Recieve
至于究竟用哪一种实现方式,由MPI自己定,通常message如果小,就用buffer的方式,如果message的大小超过了MPI内定的阈值,则用block的方式。
4) 如果MPI_Recieve找不到相应的Send怎么办?
答:
该程序将会被挂起