Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1800382
  • 博文数量: 438
  • 博客积分: 9799
  • 博客等级: 中将
  • 技术积分: 6092
  • 用 户 组: 普通用户
  • 注册时间: 2012-03-25 17:25
文章分类

全部博文(438)

文章存档

2019年(1)

2013年(8)

2012年(429)

分类: 云计算

2012-07-12 08:56:00

点对点通信是MPI库提供的基础通信设施。

点对点通信在概念上很简单:一个进程发送消息,而一个进程接收。但现实中它没那么简单。例如,一个进程可能有许多等待接收的消息。这种情况下,一个关键的问题是MPI和接收进程如何决定要接收什么消息。

另一个问题是发送和接收例程是初始化通信操作并立即返回,还是在返回前等待初始化通信操作完成。在两种情况下底下的通信操作都是相同的,但是编程接口确很不相同。


基础

源和目标:点对点通信设施是双方的,需要双方进程的共同主动参与。一个进程(源)发送,另一个进程(目标)接收。

通常,源和目标进标异步操作。哪怕是单个消息的发送和接收通常也不是同步的。源进程发送消息后,可能要很久之后目标进程才会接收它。或者目标进程可能在消息还未发出时就做好准备接收它。

因 为发送和接收通常不是同步的,所以进程可能会有一个或多个已经发送但没有被接收的消息。这些发送了但没被接收的消息被称为待处理的(pending)消 息。MPI一个很重要的特性是待处理的消息不是由一个简单的FIFO队列维护,相反,每个待处理消息都有几个属性,同时目标进程(接收进程)可以使用这些 属性来决定接收哪些消息。


消息:由信封(envelop)和消息主体(message body)两部分组成。

信封和包装信件的纸质信封相似。它包含目标地址、回信地址、以及任何其它传送和分发信件所需的信息,比如服务类型(如空邮)。

MPI消息的信封有4部分:

1、源:发送进程;

2、目标:接收进程;

3、通信者:指明源和目标同时所属的一个进程组;

4、标签(tag):用于分类消息。

标签域是必需的,但它的使用交由程序决定。一对通信进程可以使用标签的值来区分消息的类型。例如,一个标签值可以用于包含数据的消息,而另一个标签用于包含状态信息的消息。


消息主体由三部分组成:

1、缓冲区:消息数据;

2、数据类型:消息数据的类型;

3、计数器:缓冲区包含的具有该数据类型的项数。

可以把缓冲区想像成一个数组,维度由计数器决定,数组元素的类型由数据类型给出。使用数据类型和计数器,而不是字节和字节数,可以平滑地处理结构化数据和不连续的数据。同时还允许异构主机间的透明通信支持。


发送和接收消息:发送消息很直接。源(发送者的ID)隐式地被确定,但消息的其它部分(信封和主体)由发送进程显式地给出。

接收消息没有这么简单,一个进程可能有多个待处理的消息。

要接收一个消息,一个进程指明一个消息信封,MPI把它和待处理消息比对。如果匹配,则接收消息。否则,一直等到匹配的消息发送,才结束接收操作。

此外,接收消息的进程必须提供存储,消息的主体可以拷贝入内。接收进程必须小心,要提供足够的存储来存放整个消息在。


阻塞发送和接收

MPI_Send和MPI_Recv两个函数是MPI里基本的点对点通信例程。两个函数都会阻塞调用进程,直到通信操作完成。阻塞可能会造成死锁。

发送消息:MPI_Send

函数原型为:

int MPI_Send(void *buf, int count, MPI_Datatype dtype, int dest, int tag, MPI_Comm comm);

所有的参数都是输入参数,前三个是消息主体,后三个是消息信封(源进程隐式定义)。函数返回一个错误码。

接收消息:MPI_Recv

函数原型:

int MPI_Recv(void *buf, int count, MPI_Datatype dtype, int source, int tag, MPI_Comm comm, MPI_Status *status);

前三个参数是消息主体,后三个参数是信封(目标进程隐式定义)。source和tag都可以使用通配符,表示任何进程和任何标签,否则只接收特定发送进程的含特定标签的消息。通信者不能使用通配符。如果收到的消息比接收进程准备接受的数据大时,会出错。

最后一个参数是关于接收的消息的信息。当source或tag使用通配符时,可以从status里获取这些信息。同时还能得到真正接收到的数据的数量。

buf和status是输出参数,其余都是输入参数。

返回值是一个错误码。


示例代码

#include
#include

void main(int argc, char **argv)
{
    int myrank;
    MPI_Status status;
    double a[100];

    MPI_Init(&argc, &argv);
    MPI_Comm_rank(MPI_COMM_WORLD, &myrank);
    if (myrank == 0)
        MPI_Send(a, 100, MPI_DOUBLE, 1, 17, MPI_COMM_WORLD);
    else if (myrank == 1)
        MPI_Recv(a, 100, MPI_DOUBLE, 0, 17, MPI_COMM_WORLD, &status);

    MPI_Finalize();
}


运行时的行为

根据模型,使用MPI_Send发送消息时,以下事情之一可能会发生:

1、消息被拷贝到MPI内部缓冲,并稍后在后台传送到目标;或者

2、消息原封不动地放在程序的变量里,直到目标准备好接收时,消息才被传送到目标。

第一个选项允许发送进程在拷贝完成时着手做其它事情。第二个选项最小化了拷贝和内存使用,但倒致发送进程的额外延迟。这个延迟可能会很显著。

令人惊奇的是,选项1里,在任何非本地动作发生甚至开始之前,也就是说任何与发送消息相关的事情发生之间,MPI_Send调用可能就返回了。选项2隐含了发送者和接收者之间的同步。

总结起来,根上面的模型草案,当使用MPI_Send发送消息时,消息可能立即被缓冲并稍后异步地分发出去,或者发送与接收进程同步。


阻塞与完成

MPI_Send和MPI_Recv都会阻塞调用进程。两者都在所调用的通信操作完成里返回。

MPI_Recv完成的意思很简单直观--一个匹配的消息已经到达,消息的数据被拷贝到调用的输出参数里。换句话说,传递给MIP_Recv的变量包含了一个消息并已经可以使用了。

对于MPI_Send,完成的意思简单但不直观。当调用指定的消息已经交给MPI时MPI_Send调用完成。换句话说,传递给MPI_Send的变量现在可以被覆写并重用。前面已经提到有两个选项。

如果传递给MPI_Send的消息比MIP可用的内部缓冲大,那么缓冲不能被使用。在这种情况下,发送进程必须阻塞,直到目标进程开始接收这个消息,或直到有更多可用的缓冲。一般说来,被拷贝进MPI内部缓冲的消息会占用缓冲空间,直到目标进程开始接收这个消息。

注意MPI_Recv调用在匹配的待处理的消息的信封(源、标签、通信者)后会接收消息。对于正确的执行来说数据类型匹配也是需要的,但MPI不会检查它。相反,程序员负责数据类型的匹配。


死锁

当两个或多个进程阻塞并都等待另一个进程的执行时,死锁发生。由于都依赖于对方先开始执行,所以没有一个进程可以继续执行。

下面的代码会造成死锁:

    if (myrank == 0) {

        MPI_Recv(b, 100, MPI_DOUBLE, 1, 19, MPI_COMM_WORLD, &status);
        MPI_Send(a, 100, MPI_DOUBLE, 1, 17, MPI_COMM_WORLD);

    }
    else if (myrank == 1) {
        MPI_Recv(a, 100, MPI_DOUBLE, 0, 17, MPI_COMM_WORLD, &status);

        MPI_Send(a, 100, MPI_DOUBLE, 0, 19, MPI_COMM_WORLD);

    }

死锁的原因是两个进程都先接收再发送。

通常避免死锁需要在程序中小心地组织通信。程序员应该能够解释程序为何有(或没有)死锁。

下面的代码解决了上面代码的死锁问题:

    if (myrank == 0) {

        MPI_Recv(b, 100, MPI_DOUBLE, 1, 19, MPI_COMM_WORLD, &status);
        MPI_Send(a, 100, MPI_DOUBLE, 1, 17, MPI_COMM_WORLD);

    }
    else if (myrank == 1) {

        MPI_Send(a, 100, MPI_DOUBLE, 0, 19, MPI_COMM_WORLD);

        MPI_Recv(a, 100, MPI_DOUBLE, 0, 17, MPI_COMM_WORLD, &status);

    }

进程0先接收后发送,而进程1先发送再接收,从而避免死锁。

下面的代码略有改动:

    if (myrank == 0) {

        MPI_Send(a, 100, MPI_DOUBLE, 1, 17, MPI_COMM_WORLD);

        MPI_Recv(b, 100, MPI_DOUBLE, 1, 19, MPI_COMM_WORLD, &status);

    }
    else if (myrank == 1) {

        MPI_Send(a, 100, MPI_DOUBLE, 0, 19, MPI_COMM_WORLD);

        MPI_Recv(a, 100, MPI_DOUBLE, 0, 17, MPI_COMM_WORLD, &status);

    }

两个进程都先发送后接收。当MPI的缓冲可用时,死锁不会出现。但当消息数量增多后,程序迟早会发生死锁。

通常,依赖于MPI内部缓冲来避免死锁会使得程序降低可移植性和扩展性。写程序的最好方式是不论MPI内部缓冲如何都能保证运行完成。


非阻塞发送和接收

MPI提供了另一种方式来执行发送和接收操作。我们可以分离发送和接收操作的初始化和它们的完成。这通过两个分离的MPI调用来完成。第一个调用初始化操作,第二个调用完成它。在这两个操作之间,程序可以做任何其它事情。

两个分离的调用的底层的通信操作和单个调用是一样的,但是接口不同。


投递、完成、和请求句柄

每个通信操作的发送和接收的非阻塞的接口需要两个调用:一个初始化操作,第二个完成它。初始化一个发送调用被称为投递一个发送请求。初始化一个接收操作被称为投递一个接收请求。

一旦一个发送或接收操作被投递,MPI提供了两种不同的方式来完成它。一个进程可以测试操作是否完成,而不需要阻塞在完成操作上。另一种方式是,它可以等待操作完成。

在调用一个非阻塞例程投递一个发送或接收操作后,投递进程需要某种方式来引用被投递的操作。MPI使用请求句柄来达到这个目的。非阻塞发送和接收例程都返回请求句柄,它可用来标识被投递的操作。

总而言之,发送和接收操作可以通过非阻塞例程被投递(初始化)。投递操作由请求句柄标识。通过请求句柄,进程可以检查已投递的操作的状态或等待它们的完成。


非阻塞地投递发送请求

MPI_Isend可以投递一个发送请求而不会阻塞在等待完成上。

函数原型:

int MPI_Isend(void *buf, int count, MPI_Datatype dtype, int dest, int tag, MPI_Comm comm, MPI_Request *request);

参数与MPI_Send相似,但多了request的输出参数。参数被传递给MPI_Isend后不能被读写,直到发送操作完成。

返回值为错误码。


非阻塞地投递接收请求

MPI_Irecv可以投递一个接收请求而不会阻塞在等待完成上。

函数原型:

int MPI_Irecv(void *buf, int count, MPI_Datatype dtype, int source, int tag, MPI_Comm comm, MPI_Request *request);

参数与MPI_Recv相似,但多了request的输出参数。参数被传递给MPI_Irecv后不能被读写,直到接收操作完成。

返回值为错误码。

完成:等待或检测

投递的发送和接收必须完成。如果发送和接收操作由一个非阻塞例程投递,那么它的完成状态可以通过调用一组完成例程来检查。MPI同时提供了阻塞和非阻塞完成例程。阻塞例程为MPI_Wait以及它的变体。非阻塞例程为MPI_Test以及它的变体。


等待

MPI_Wait的函数原型:

int MPI_Wait(MPI_Request *request, MPI_Status *status);

reuest参数为已投递的发送和接收操作的请求句柄。status为输出参数,接收操作时包含接收到的消息的信息(源、标签、真实接收的数据数量等),发送操作时可能包含一个错误码(表示发送是否出错,而MPI_Wait本身返回的错误码不同)。

返回一个错误码。


检测:

MPI_Test的函数原型:

int MPI_Test(MPI_Request *request, int *flag, MPI_Status *status);

request和status参数和MPI_Wait基本相同。flag为输出参数,如果发送或接收完成时值为true。当flag值为false时status的值没有定义。


非阻塞发送和接收的优缺点

非阻塞例程可以更容易地写出无死锁的程序。

在延迟很大的系统上,很早地投递接收操作经常是高效简单的方法,来掩盖通信的开销。在物理分布的主机群(比如工作站簇)上延迟会很大,而共享内存的多处理器会相对较小。一般来说掩盖通信的开销需要在算法和代码结构上很小心。

从坏的方面来说,非阻塞发送和接收例程会增加代码复杂性,这让代码更难调试和维护。


示例代码

#include

void main(int argc, char **argv)
{
    int myrank;
    MPI_Request request;
    MPI_Status status;
    double a[100], b[100];

    MPI_Init(&argc, &argv);
    MPI_Comm_rank(MPI_COMM_WORLD, &myrank);
    if (myrank == 0) {
        MPI_Irecv(b, 100, MPI_DOUBLE, 1, 19, MPI_COMM_WORLD, &request);
        MPI_Send(a, 100, MPI_DOUBLE, 1, 17, MPI_COMM_WORLD);
        MPI_Wait(&request, &status);
    } else if (myrank == 1) {
        MPI_Irecv(b, 100, MPI_DOUBLE, 0, 17, MPI_COMM_WORLD, &request);
        MPI_Send(a, 100, MPI_DOUBLE, 0, 19, MPI_COMM_WORLD);
        MPI_Wait(&request, &status);
    }
    MPI_Finalize();
}

上面的代码没有死锁。


发送模式

MPI提供了以下四种发送模式:

1、标准模式发送;

2、同步模式发送;

3、预准备模式发送;

4、缓冲模式发送。

标准模式是最广泛使用的。

接收模式只有一种。不论发送模式如何,MPI_Recv和MPI_Irecv的调用是相同的。

对于四种发送模式,阻塞和非阻塞调用都可用。


标准模式发送

标准模式是MPI的通用发送模式。其它三个模式在特殊的条件下有用。

如之前所述,标准模式下,消息或拷贝到MPI内部缓冲里并稍后发送,或源和目标进程同步。MPI实现可以自由选择缓冲或同步,或视情况而定,比如消息大小,可用资源,等。

标准模式发送的好处是MPI可以基于情况选择缓冲和同步。通常MPI在进行权衡方面,特别在涉及底层资源和MPI内部资源的时候,会做得更好。


同步模式发送要求MPI同步发送和接收进程。当同步模式发送操作完成时,发送进程可以假设接收进程已经开始接收消息了。目标进程不需要完成接收,但是必须已经开始接收了。


预准备模式需要在发送之前,目标进程已经投递了一个匹配的接收。如果目标没有投递接收,则结果没有定义。你需要保证这个条件得到满足。在某些情况下,不需要额外的工作就可以知道目标进程的状态信息。当知道接收已经被投递时,MPI可以通过在内部使用更短的协议来降低通信开销。


缓冲模式发送要求MPI使用缓冲。这种方式的不足是你必须负责管理缓冲。在任何时候,可用缓冲空间不足以完成调用时,结果没有定义。函数MPI_Buffer_attach和MPI_Buffer_detach为MPI制造可用的缓冲。


各模式对应的函数

发送模式阻塞函数非阻塞函数
标准MPI_SendMPI_Isend
同步MPI_SsendMPI_Issend
预准备MPI_RsendMPI_Irsend
缓冲MPI_BsendMPI_Ibsend

其它模式的函数参数(阻塞与非阻塞)与标准模式的函数参数相同。


阅读(6128) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~