1.MPI阻塞式通信的一个例子
先看下面这段程序,可以看到每个进程都调用了两次MPI_Bcast()
,其中第一次广播的数据为字符串HelloStr
的长度,第二次广播的数据为HelloStr
字符串本身。所以可以很容易地理解到,0号进程要向其他进程广播一个任意长度地字符串时,需要分为两步,第一次先向组中的其他进程广播字符串的长度,第二次广播字符串本身;其他进程在接收时,要先接收到从0号进程发送来的字符串长度,申请该长度的内存,然后接收字符串。
这里的MPI_Bcast()
属于阻塞式广播。所谓阻塞,就是在调用返回结果之前,当前进程会一直阻塞在那里,即进程悬挂。所以阻塞式发送可以理解为,只有进程在确定消息完全发出去(发送到缓冲区)后才会继续执行下一条指令,在此之前会一直等待;阻塞式接受也可以理解为,当前进程在未接受到完整的消息时,会一直等待,直到接收结束才会继续执行。
#include <iostream>
#include <mpi.h>
int main(int argc, char* argv[])
{
MPI_Init(&argc, &argv);
int RankID;
MPI_Comm_rank(MPI_COMM_WORLD, &RankID);
if (0 == RankID)
{
char HelloStr[] = "Hello,Guys!";
int StrLength = _countof(HelloStr);
std::cout << StrLength << std::endl;
MPI_Bcast(&StrLength, 1, MPI_INT, 0, MPI_COMM_WORLD);
MPI_Bcast(HelloStr, StrLength, MPI_CHAR, 0, MPI_COMM_WORLD);
}
else
{
int RecvStrLength = 0;
MPI_Bcast(&RecvStrLength, 1, MPI_INT, 0, MPI_COMM_WORLD);
char *RecvHelloStr = new char[RecvStrLength];
MPI_Bcast(RecvHelloStr, RecvStrLength, MPI_CHAR, 0, MPI_COMM_WORLD);
std::cout << RankID << ": " << RecvHelloStr << std::endl;
delete RecvHelloStr;
}
MPI_Finalize();
return 0;
}
以上这段程序中,进程0发送完StrLength
后才会执行发送字符串的指令,其他进程确定接收到RecvStrLength
后才会开始接收字符串。
我在这里创建了5个进程,执行指令:mpiexec -n 5 MPITest.exe
运行的结果为:
2.MPI中的非阻塞式通信
阻塞式虽然用起来很稳定,也确实能够解决很多问题,但当进程挂起时,除了持续等待,进程是无法向下继续执行的,这就造成了资源上的浪费。另外,当进程间通信时分别需要来自其他进程的消息时,就可能会出现死锁,即所有进程都在等待其他进程发送消息,但事实上却不可能有进程会发送消息。非阻塞式和阻塞式相对应,即使调用不能立刻返回结果,进程也不会因此而挂起,而是继续向下执行。所以非阻塞式通信中在发送和接收时不需要等到完全发送或完全接收后才能执行下一条指令。
MPI中非阻塞广播的使用可以参考下面程序,这是基于上面的代码改的,进程0向其他进程广播一个int型整数(StrLength
):
#include <iostream>
#include <mpi.h>
int main(int argc, char* argv[])
{
MPI_Init(&argc, &argv);
int RankID;
MPI_Request Request;
MPI_Status Status;
MPI_Comm_rank(MPI_COMM_WORLD, &RankID);
if (0 == RankID)
{
char HelloStr[] = "Hello,Guys!";
int StrLength = _countof(HelloStr);
std::cout << StrLength << std::endl;
MPI_Ibcast(&StrLength, 1, MPI_INT, 0, MPI_COMM_WORLD, &Request);
MPI_Wait(&Request, &Status);
}
else
{
int RecvStrLength = 0;
MPI_Ibcast(&RecvStrLength, 1, MPI_INT, 0, MPI_COMM_WORLD, &Request);
MPI_Wait(&Request, &Status);
std::cout <<RankID << ": " << RecvStrLength << std::endl;
}
MPI_Finalize();
return 0;
}
程序执行的结果为:
再看代码,可以看到18行和27行我们改成了用非阻塞式广播的方式发送和接收一个整数(字符串长度)。需要注意的是19行和28行的MPI_Wait(&Request, &Status);
,这两处的MPI_Wait()
是很重要的:
- 28行:读者可以尝试去掉该指令进行尝试,会发现程序直接报错(
[0]Fatal error in MPI_Ibcast: Other MPI error, error stack: MPI_Ibcast(…) failed. failed to attach to a bootstrap queue
)。这是因为接收端必须调用MPI_Wait()
来确保收到的数据是有效的,因此该行指令是必须要有的,否则就不能接收到RecvStrLength
的值。 - 19行:与28行的不同,我们会发现即使删掉这一行指令也不会影响到最终的结果。但这行指令也是需要的。如当存在这种情况时,
MPI_Ibcast(&StrLength, 1, MPI_INT, 0, MPI_COMM_WORLD, &Request); StrLength = 5;
在广播之后立即修改变量StrLength
,是不安全的,因为当我们修改变量StrLength
的值时,可能还没有开始数据传输或者没有完成,MPI就可能会读取它,此时发送的StrLength
的值就会变成5,这是一种竞争情况。而避免这种竞争情况的方法就是在MPI_Ibcast()
之后添加一行MPI_Wait()
。
3.非阻塞式的性能
由于异步接收和发送的特性,发送进程可以在接收进程调用MPI_Irecv之前调用MPI_Isend发送数据。在这种情况下,MPI实现会暂时将数据存储在接收进程所在主机的内部缓冲区中。但是,如果在发送进程调用MPI_Isend发送数据前,接收进程已经调用MPI_Irecv准备好接收数据,那么MPI实现就只是简单的填充程序提供的缓冲区(发送,接收缓冲区)。而不用使用MPI提供的内部缓冲区。这可以有效的去掉发生在接收进程端的内存拷贝操作,对于大数据量的处理来说,这会极大的提高性能。
因此,当发送大量数据时,最好在发送进程调用MPI_Isend之前,接收进程先调用MPI_Irecv。很多时候,这很难实现,但还是可以的,这可以帮你的程序挤出一些额外的性能。