MPI分布式内存编程(一):预备知识

编译与执行

以打印来自进程问候语句的MPI程序(mpi_hello.c)为例:

#include<stdio.h>
#include<string.h>
#include<mpi.h>

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{
        printf("Greetings from process %d of %d!
", 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
", greeting);
        }
    }
    MPI_Finalize();
    return 0;
}

编译程序:

mpicc -g -Wall -o mpi_hello mpi_hello.c

启动程序:

# mpiexec -n <number of processes> ./mpi_hello

#用1个进程运行程序
mpiexec -n 1 ./mpi_hello

#用4个进程运行程序
mpiexec -n 4 ./mpi_hello

MPI程序

  • 需包含<mpi.h>头文件,其中包括了MPI函数的原形、宏定义、类型定义等,还包括了编译MPI程序所需要的全部定义与声明。
  • 所有MPI定义的标识符都由字符串MPI_开始。下划线后的第一个字母大写,表示函数名和MPI定义的类型。MPI定义的宏和常量的所有字母都是大写的。

MPI_Init和MPI_Finalize

MPI_Init

int MPI_Init(
	int* argc_p		/*in/out*/,
	char*** argv_p	/*in/out*/);

调用MPI_init是为了告知MPI系统进行所有必要的初始化设置,如为消息缓冲区分配存储空间,为进程指定进程号等。在调用MPI_Init前,不应该调用其他MPI函数。

参数argc_pargv_p是指向参数argc和argv的指针。当不使用这些参数时,设置为NULL即可。

返回值为一个int型错误码,大部分情况下忽略。

MPI_Finalize

int MPI_Finalize(void);

调用MPI_init是为了告知MPI系统MPI已经使用完毕,为MPI而分配的任何资源都可以释放了。在调用MPI_Finalize之后,不应该再调用MPI函数了。

通信子、MPI_Comm_size和MPI_Comm_rank

在MPI中,通信子(communicator)指的是一组可以互相发送消息的进程集合。MPI_Init的其中一个目的,是在启动程序时,定义由用户启动的所有进程所组成的通信子。这个通信子称为MPI_COMM_WORLD

MPI_Comm_size和MPI_Comm_rank可以获取关于MPI_COMM_WORLD的信息:

int MPI_Comm_size(
	MPI_Comm	comm		/* in */,
	int*		comm_sz_p	/* out */);
int MPI_Comm_rank(
	MPI_Comm	comm		/* in */,
	int*		my_rank_p	/* out */);

这两个函数中,第一个参数是一个通信子,它所属的类型是MPI为通信子定义的特殊类型:MPI_Comm。MPI_Comm_size函数在第二个参数里返回通信子的进程数;MPI_Comm_rank函数在第二个参数里返回正在调用进程在通信子中的进程号。

在MPI_COMM_WORLD中经常使用参数comm_sz表示进程的数量,用参数comm_rank表示进程号。

SPMD程序

大部分并行编程中,编写一个单个程序,让不同进程产生不同动作。实现方式是,简单地让进程按照它们的进程号来匹配程序分支。这一方法称为单程序多数据流(Single Program, Multiple Data, SPMD)。

MPI_Send

int	MPI_Send(
	void*			msg_buf_p	/* in */,
	int 			msg_size	/* in */,
	MPI_Datetype	msg_type	/* in */,
	int 			dest		/* in */,
	int 			tag			/* in */,
	MPI_Comm		communicator/* in */);

前三个参数,msg_buf_pmsg_sizemsg_type定义了要发送的消息的内容。剩下的参数,desttagcommunicator定义了消息的目的地。

第一个参数msg_buf_p是一个指向包含消息内容的内存块的指针。

第二个和第三个,msg_size和msg_type,指定了要发送的数据量。

因为c语言中的类型(int、char等)不能作为参数传递给函数,所以MPI定义了一个特殊类型:MPI_Datatype,用于参数msg_type。部分数据类型对照如下表所示:

MPI数据类型 c语言数据类型
MPI_CHAR signed char
MPI_SHORT signed short
MPI_INT signed int

第四个参数dest指定了要接收消息的进程的进程号。

第五个参数tag是个非负int型,用于区分看上去完全一样的消息。

第六个参数communicator是一个通信子。所有涉及通信的MPI函数都有一个通信子参数。通信子最重要的目的之一是指定通信范围。通信子指的是一组可以互相发送消息的进程的集合。反过来,一个通信子中的进程所发送的消息不能被另一个通信子中的进程所接收。

MPI_Recv

int	MPI_Recv(
	void*			msg_buf_p	/* out */,
	int 			msg_size	/* in */,
	MPI_Datetype	msg_type	/* in */,
	int 			source		/* in */,
	int 			tag			/* in */,
	MPI_Comm		communicator/* in */,
    MPI_Status*		status_p	/* out */);

MPI_Recv的前六个参数对应了MPI_Send的前六个参数。因此,前三个参数指定了用于接收消息的内存,后面的三个参数用来识别消息:参数source指定了接收的消息应该从哪个进程发送而来,参数tag要与发送消息的参数tag相匹配,参数communicator必须与发送进程所用的通信子相匹配。

第七个参数status_p,在大部分情况下,调用函数并不使用这个参数,赋予其特殊的MPI常量MPI_STATUS_IGNORE即可。

消息匹配

假定q号进程调用了MPI_Send函数:

MPI_Send(send_buf_p, send_buf_sz, send_type, dest, send_tag, send_comm);

并且假定r号进程调用了MPI_Recv函数:

MPI_Recv(recv_buf_p, recv_buf_sz, recv_type, src, recv_tag, recv_comm, &status);

则q号进程调用MPI_Send函数所发送的消息可以被r号进程调用MPI_Recv函数接收,如果:

  • recv_comm = send_comm
  • recv_tag = send_tag
  • dest = r
  • src = q

然而,这些条件还不足以使消息可以成功地接收。前三对参数还必须执行兼容的缓冲区。大多数时候,满足下面的规则即可:

  • 如果recv_type = send_type,同时recv_buf_sz (ge) send_buf_sz,那么由q号进程发送点的消息就可以被r号进程成功的接收。

一个进程可以接收多个进程发来的消息,接收进程并不知道其他进程发送消息的顺序。MPI提供了一个特殊的常量MPI_ANY_SOURCE,可以传递给MPI_Recv,这样,如果0号进程执行下列代码,那么他可以按照进程完成工作的顺序来接收结果:

for(int 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);
}

(如果不是使用常量MPI_ANY_SOURCE而是i,即按进程号顺序接收结果,则可能会产生进程号靠后却先完成的任务需要等待前面的进程完成的问题)

类似地,一个进程也有可能接收多条来自另一个进程的有着不同标签的消息,并且接收进程并不知道消息发送的顺序。在这种情况下,MPI提供了特殊常量MPI_ANY_TAG,可以传给MPI_Recv的参数tag。

请注意:

  1. 只有接收者可以使用通配符参数(MPI_ANY_SOURCE、MPI_ANY_TAG等)。发送者必须指定一个进程号与一个非负整数标签。MPI使用的是“推”(push)通信机制,而不是“拉”(pull)通信机制。
  2. 通信子参数没有通配符。发送者和接收者都必须指定通信子。

MPI_Send和MPI_Recv的语义

发送进程组装信息,一旦组装完毕,有两种可能性:发送进程可以缓冲消息,也可以阻塞。如果它缓冲消息,MPI系统会把消息放置在它自己的内部存储器里,并返回MPI_Send的调用。如果系统发生阻塞,那么它将一直等待,直到可以开始发送消息,并不立即返回对MPI_Send的调用。因此,如果使用MPI_Send,当函数返回时,实际上并不知道消息是否已经发送出去,只知道消息所用的存储区,即发送缓冲区,可以被程序再次使用。

MPI_Send的精确行为是由MPI实现所决定的。但是,典型的实现方法有一个默认的消息“截止”大小。如果一条消息的大小小于“截止”大小,它将被缓冲;如果大于“截止”大小,那么MPI_Send函数将被阻塞。

与MPI_Send函数不同,MPI_Recv函数总是阻塞的,直到收到一条匹配的消息。因此,当MPI_Recv函数调用返回时,就知道一条消息已经存储在接收缓冲区中了(除非产生了错误)。

MPI要求消息是不可超越的。即如果q号进程发送了两条消息给r号进程,那么q进程发送的第一条消息必须在第二条消息之前可用。但是,如果消息是来自不同进程的,消息的到达顺序是没有限制的。

潜在的陷阱

如果一个进程试图接收消息,但没有相匹配的消息,那么该进程将会被永远阻塞在那里,即进程悬挂。因此,在设计程序时,需要保证每条接收都有一条相匹配的发送。

如果MPI_Send和MPI_Recv的标签(tag)不匹配,或者目标进程的进程号与源进程的进程号不相同,那么接收与发送就无法相匹配了,这会导致一个进程悬挂起来,或者可能造成接收端匹配到另一个发送端。

简单的说,如果调用MPI_Send发生了阻塞,并且没有相匹配的接收,那么发送进程就悬挂起来。另一方面,如果调用MPI_Send被缓冲,但没有相匹配的接收,那么信息将会丢失。

参考

《并行程序设计导论》 3.1

原文地址:https://www.cnblogs.com/wangzhebufangqi/p/14118100.html