欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

并行编程之MPI使用简析(1)

程序员文章站 2022-07-12 21:34:44
...

本文的实例代码来自于 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_pargv_p是指向main函数种argc, argv的指针,但是现在我们用不到,所以把两者都设为NULL
MPI_Finalize(void);是用来告诉MPI系统:“嘿,我用完MPI了,分给MPI的资源现在可以释放了!”
概括来说,其他的MPI函数必须在MPI_InitMPI_Finalize之间调用。


MPI_COMM_WORLD

在介绍MPI_SendMPI_Recieve之前先要讲一下什么是Communicator也就是代码中出现的MPI_COMM_WORLD

MPI_COMM_WORLDMPI_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_SendMPI_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怎么办?

答:
该程序将会被挂起