文章大纲引言一、MPI 概述二、MPI基础数据类型1、使用MPI_Type_create_struct创建结构体类型数据类型1.1、在C中定义结构体并确定成员偏移量1.2、创建自定义 MPI 数据类型2、使用MPI_Type_create_subarray创建子数组类型数据类型2.1、确定子数组的形状和位置2.2、创建自定义 MPI 数据类型三、通讯器communicator四、发送和接收五 、消息标签TAG 的使用六、MPI_Status 结构体https://github.com/mpitutorial/https://www.open-mpi.org/引言在 90 年代之前程序员可没我们这么幸运。对于不同的计算架构写并发程序是一件困难而且冗长的事情。当时很多软件库可以帮助写并发程序但是没有一个大家都接受的标准来做这个事情。在当时大多数的并发程序只出现在科学和研究的领域。最广为接受的模型就是消息传递模型指程序通过在进程间传递消息消息可以理解成带有一些信息和数据的一个数据结构来完成某些任务。举例来说主进程master process可以通过对从进程slave process发送一个描述工作的消息来把这个工作分配给它。另一个例子就是一个并发的排序程序可以在当前进程中对当前进程可见的我们称作本地的locally数据进行排序然后把排好序的数据发送的邻居进程上面来进行合并的操作。几乎所有的并行程序可以使用消息传递模型来描述。一、MPI 概述由于当时很多软件库都用到了这个消息传递模型但是在定义上有些微小的差异这些库的作者以及一些其他人为了解决这个问题就在 Supercomputing 1992 大会上定义了一个消息传递接口的标准也就是 MPI。这个标准接口使得程序员写的并发程序可以在所有主流的并发框架中运行。并且允许他们可以使用当时已经在使用的一些流行库的特性和模型。到 1994 年的时候一个完整的接口标准定义好了MPI-1。我们要记住 MPI 只是一个接口的定义而已。然后需要程序员去根据不同的架构去实现这个接口。很幸运的是仅仅一年之后一个完整的 MPI 实现就已经出现了。在第一个实现之后MPI 就被大量地使用在消息传递应用程序中并且依然是写这类程序的标准de-facto。MPI是一套定义了多个原语的消息传递接口这一接口主要被用于多进程间的通信。它的竞品包括 RPCDistributed Shared Memory 等二、MPI基础数据类型当然也支持创建自己的 MPI 数据类型来构建更复杂的消息类型。1、使用MPI_Type_create_struct创建结构体类型数据类型1.1、在C中定义结构体并确定成员偏移量//假设传递一个包含整数和浮点数的结构体structComplexData{intintegerPart;floatfloatPart;};然后需要确定每个成员在结构体中的偏移量偏移量是以字节为单位的距离从结构体的起始位置到成员的起始位置的距离。可以使用offsetof宏在stddef.h头文件中来计算偏移量。对于上述结构体整数部分的偏移量为 0 字节因为它是结构体的第一个成员浮点数部分的偏移量可以通过offsetof(struct ComplexData, floatPart)来计算得到的结果是sizeof(int)字节因为整数成员占sizeof(int)字节。1.2、创建自定义 MPI 数据类型使用MPI_Type_create_struct函数来创建自定义数据类型。这个函数的参数包括结构体成员的数量、每个成员的块长度对于基本类型通常为 1、每个成员的位移偏移量、每个成员的 MPI 数据类型以及一个用于存储新创建的数据类型的变量。MPI_Datatype customDataType;intblockLengths[2]{1,1};MPI_Aint displacements[2];MPI_Datatype types[2]{MPI_INT,MPI_FLOAT};displacements[0]0;displacements[1]sizeof(int);MPI_Type_create_struct(2,blockLengths,displacements,types,customDataType);MPI_Type_commit(customDataType);首先定义了数组blockLengths来表示每个成员的块长度都是 1displacements数组用于存储成员的偏移量types数组用于存储成员对应的 MPI 基本数据类型。然后调用MPI_Type_create_struct创建自定义数据类型customDataType最后通过MPI_Type_commit提交这个数据类型使其可以在 MPI 通信中使用。MPI_Send(complexData, 1, customDataType, destinationRank, tag, MPI_COMM_WORLD);MPI_Recv(receivedComplexData, 1, customDataType, sourceRank, tag, MPI_COMM_WORLD, MPI_STATUS_IGNORE);2、使用MPI_Type_create_subarray创建子数组类型数据类型2.1、确定子数组的形状和位置假设要传递一个二维数组的子数组首先需要确定子数组在原始二维数组中的位置和形状。例如有一个int array[10][10]的二维数组要传递一个从array[3][3]开始形状为3x3的子数组。2.2、创建自定义 MPI 数据类型使用MPI_Type_create_subarray函数来创建子数组类型的数据类型。这个函数的参数包括数组的维度数、原始数组的形状以整数数组表示、子数组的形状以整数数组表示、子数组在原始数组中的起始位置以整数数组表示、数据的排列顺序如MPI_ORDER_C表示按行存储MPI_ORDER_FORTRAN表示按列存储以及一个用于存储新创建的数据类型的变量。例如intarraySizes[2]{10,10};intsubarraySizes[2]{3,3};intstarts[2]{3,3};MPI_Datatype subarrayDataType;MPI_Type_create_subarray(2,arraySizes,subarraySizes,starts,MPI_ORDER_C,MPI_INT,subarrayDataType);MPI_Type_commit(subarrayDataType);首先定义了原始数组的大小arraySizes、子数组的大小subarraySizes和子数组的起始位置starts然后调用MPI_Type_create_subarray创建自定义数据类型subarrayDataType最后通过MPI_Type_commit提交这个数据类型以便在 MPI 通信中用于传递子数组。简单使用自定义的数组MPI_Send(subarray, 1, subarrayDataType, destinationRank, tag, MPI_COMM_WORLD);MPI_Recv(receivedSubarray, 1, subarrayDataType, sourceRank, tag, MPI_COMM_WORLD, MPI_STATUS_IGNORE);三、通讯器communicator通讯器定义了一组能够互相发消息的进程。在这组进程中每个进程会被分配一个序号称作秩rank进程间显性地通过指定秩来进行通信。通信的基础建立在不同进程间发送和接收操作。一个进程可以通过指定另一个进程的秩以及一个独一无二的消息标签tag来发送消息给另一个进程。接受者可以发送一个接收特定标签标记的消息的请求或者也可以完全不管标签接收任何消息然后依次处理接收到的数据。类似这样的涉及一个发送者以及一个接受者的通信被称作点对点point-to-point通信。四、发送和接收发送和接收是 MPI 里面两个基础的概念。MPI 里面几乎所有单个的方法都可以使用基础的发送和接收 API 来实现。MPI 的发送和接收方法是按以下方式进行的开始的时候A 进程决定要发送一些消息给 B 进程。A进程就会把需要发送给B进程的所有数据打包好放到一个缓存里面。因为所有数据会被打包到一个大的信息里面因此缓存常常会被比作信封就像我们把好多信纸打包到一个信封里面然后再寄去邮局。数据打包进缓存之后通信设备通常是网络就需要负责把信息传递到正确的地方。这个正确的地方也就是根据特定秩确定的那个进程。尽管数据已经被送达到 B 了但是进程 B 依然需要确认它想要接收 A 的数据。一旦它确定了这点数据就被传输成功了。进程 A 会接收到数据传递成功的信息然后去干其他事情。有时候 A 需要传递很多不同的消息给 B。为了让 B 能比较方便地区分不同的消息MPI 运行发送者和接受者额外地指定一些信息 ID (正式名称是标签, tags)。当 B 只要求接收某种特定标签的信息的时候其他的不是这个标签的信息会先被缓存起来等到 B 需要的时候才会给 B。MPI_Send(void*data,intcount,MPI_Datatype datatype,intdestination,inttag,MPI_Comm communicator)MPI_Recv(void*data,intcount,MPI_Datatype datatype,intsource,inttag,MPI_Comm communicator,MPI_Status*status)第一个参数是数据缓存。第二个和第三个参数分别描述了数据的数量和类型。MPI_send 会精确地发送 count 指定的数量个元素MPI_Recv 会最多接受 count 个元素第四个和第五个参数指定了发送方/接受方进程的秩以及信息的标签。第六个参数指定了使用的 communicator。第六个参数指定了使用的 communicator。MPI_Recv 方法特有的最后一个参数提供了接受到的信息的状态。五 、消息标签TAG 的使用六、MPI_Status 结构体前面仅仅只介绍了如何发送事先知道消息长度的消息。尽管可以将消息的长度作为单独的发送/接收操作发送但是 MPI 本身仅通过几个额外的函数调用即可支持动态消息。MPI_Recv 将 MPI_Status 结构体的地址作为参数可以使用 MPI_STATUS_IGNORE 忽略。 如果我们将 MPI_Status 结构体传递给 MPI_Recv 函数则操作完成后将在该结构体中填充有关接收操作的其他信息。 三个主要的信息包括发送端秩发送端的秩存储在结构体的 MPI_SOURCE 元素中。也就是说如果我们声明一个 MPI_Status stat 变量则可以通过 stat.MPI_SOURCE 访问秩。消息的标签消息的标签可以通过结构体的 MPI_TAG 元素访问类似于 MPI_SOURCE。消息的长度 消息的长度在结构体中没有预定义的元素。但我们可使用 MPI_Get_count 找出消息的长度。//MPI_Get_count 函数中使用者需要传递 MPI_Status 结构体消息的 datatype数据类型并返回 count。 变量 count 是已接收的 datatype 元素的数目。MPI_Get_count(MPI_Status*status,MPI_Datatype datatype,int*count)//MPI_Get_count 的返回值是相对于传递的数据类型而言的。 如果用户使用 MPI_CHAR 作为数据类型则返回的数量将是原来的四倍假设整数是四个字节而 char 是一个字节MPI_Recv 可以将 MPI_ANY_SOURCE 用作发送端的秩将 MPI_ANY_TAG 用作消息的标签。 在这种情况下MPI_Status 结构体是找出消息的实际发送端和标签的唯一方法。 此外并不能保证 MPI_Recv 能够接收函数调用参数的全部元素。 相反它只接收已发送给它的元素数量如果发送的元素多于所需的接收数量则返回错误。 MPI_Get_count 函数用于确定实际的接收量未完待续…