《并行计算》分布存储系统并行编程


基于分布存储的并行系统

之前我们是讲了共享存储系统的并行编程,共享存储的一个最重要的点就是多线程在同时修改一个变量时的线程安全问题,而使用分布存储系统则一般不考虑这个问题,因为不是共享的,进程之间依靠通信进行交互(没错,这里就是进程,进程之间一般是不共享数据的)。

相较于基于共享存储的OpenMP编程,分布存储使用MPI标准进行编程。MPI全称是Message Passing Interface,用于开发基于消息传递的并行程序。

配置MPI编程环境(基于Visual Studio)

前往 https://www.microsoft.com/en-us/download/details.aspx?id=57467 下载支持MS平台的MPI包,两个都要下载
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

MPI的Hello程序

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

void main(int argc, char **argv) {
	int rank, size, tag = 1;
	int senddata, recvdata;
	MPI_Status status;
	MPI_Init(&argc, &argv);
	MPI_Comm_rank(MPI_COMM_WORLD, &rank);
	MPI_Comm_size(MPI_COMM_WORLD, &size);
	if (rank == 0) {
		senddata = 9999;
		MPI_Send(&senddata, 1, MPI_INT, 1, tag, MPI_COMM_WORLD);
	}
	else if (rank == 1) {
		MPI_Recv(&recvdata, 1, MPI_INT, 0, tag, MPI_COMM_WORLD, &status);
		printf("recv: %d\n", recvdata);
	}
	MPI_Finalize();
}

如何运行这个程序?需要通过刚才下载的SDK开发包里面的mpiexec可执行文件,如下,执行:
mpiexec -n 2 ConsoleApplication.exe // n 指定进程的个数
在这里插入图片描述
可见,程序已经接收到9999的数据。

基本API的用法
  • MPI启动:int MPI_init(int* argc, char*** argv),进入MPI环境,完成所有的初始化工作
  • MPI结束:int MPI_Finalize(void),从MPI环境中退出
  • 获取进程编号:int MPI_Comm_rank(MPI_Comm comm, int* rank),获取当前进程在通信域中的编号,通过编号实现并行进程的合作
  • 获取进程数: int MPI_Comm_size(MPI_Comm comm, int* size),获取通信域中的进程个数,可根据它合理设定进程的任务量
  • 发送消息:int MPI_Send(void *buf, int count, MPI_Datatype dataytpe, int dest, int tag, MPI_Comm comm) ,将起始地址为buf的count个datatype类型的数据发送给目标进程dest,tag为数据标签
  • 接收消息:int MPI_Recv(void *buf, int count, MPI_Datatype datatyepe, int source, int tag, MPI_Comm comm, MPI_Status *status) ,从源进程source接收一个标识为tag的消息,存放在起始地址为buf的接收缓冲区,接收到的消息长度必须≤接收缓冲区的长度count
MPI中的消息

消息缓冲:存储消息的内容,由三元组<起始地址,数据个数,数据类型>标识

消息信封:消息的发送者和接收者,由三元组<源/目的进程,消息标签,通信域>标识

在这里插入图片描述

消息数据类型:预定义数据类型、派生数据类型

预定义数据类型
在这里插入图片描述
派生数据类型:可以定义由数据类型不同且地址空间不连续的数据项组成的消息,用类型图描述
在这里插入图片描述
一些构造派生数据类型的MPI接口:
在这里插入图片描述
Demo:

#include "mpi.h" 
#include <iostream>
#define N 10
using namespace std;
int main(int argv, char *argc[])
{
	int rank;
	int a[N][N];
	MPI_Init(&argv, &argc);
	MPI_Comm_rank(MPI_COMM_WORLD, &rank);
	MPI_Datatype Column_Type; // 声明一个构造类型
	MPI_Type_vector(N, 1, N, MPI_INT, &Column_Type); // 通过MPI_Type_vector定义构造类型
	MPI_Type_commit(&Column_Type); // 提交类型到MPI,后面方可使用
	if (rank == 0)
	{
		for (int i = 0; i < N; i++)
			for (int j = 0; j < N; j++)
				a[i][j] = i*N + j;
		MPI_Send(a[0], 1, Column_Type, 1, 0, MPI_COMM_WORLD);
	}
	else
	{
		for (int i = 0; i < N; i++)
			for (int j = 0; j < N; j++)
				a[i][j] = -1;
		MPI_Status status;
		MPI_Recv(a[0], 1, Column_Type, 0, 0, MPI_COMM_WORLD, &status);
		for (int i = 0; i < N; i++)
		{
			for (int j = 0; j < N; j++)
				cout << a[i][j] << "\t";
			cout << endl;
		}
	}
	MPI_Finalize();
	return 0;
}

上面代码完成将一行矩阵元素传递到另外一个进程的矩阵的一列中:
在这里插入图片描述

消息标签

标签用来区分不同的消息,如果没有用数据标签,那么进程有可能接收到不想接收的消息

通信域

包括进程组和通信上下文等内容,描述通信进程间的通信关系。

  • 进程组:进程的有限集合
  • 通信上下文:安全区别不同的通信以免相互串扰,在一个上下文中的消息发送不能在另一个上下文中被接收

MPI_COMM_WORLD:所有进程的集合

MPI_COMM_SELF:只包含使用它的进程

消息匹配

如同投信一样,地点和人物对上了才能交付。MPI也一样,通过指定的进程id将消息发往对应进程,并且消息id对得上接收的TAG,这才能匹配。

MPI_ANY_TAG

MPI_ANY_SOURCE

消息匹配 Demo:

#include "mpi.h"
#include <stdio.h>
void main(int argc, char ** argv)
{
	int rank, size;
	MPI_Status status;
	MPI_Init(&argc, &argv);
	MPI_Comm_rank(MPI_COMM_WORLD, &rank);
	MPI_Comm_size(MPI_COMM_WORLD, &size);
	if (rank == 0)
	{
		int a[10];
		for (int i = 0; i < 10; i++)
			a[i] = i;
		MPI_Send(a, 10, MPI_INT, 1, 1, MPI_COMM_WORLD);
	}
	else if (rank == 1)
	{
		int a[10]; 
		// 源、TAG对应得上,则接收消息
		MPI_Recv(a, 10, MPI_INT, 0, 1, MPI_COMM_WORLD, &status);
		for (int i = 0; i < 10; i++)
			printf("%d\n", a[i]);
	}
	MPI_Finalize();
}

消息状态

MPI_Recv函数返回时将在MPI_Status指示的变量中存放实际接收消息的状态信息

消息状态包含了一些信息:源进程标识、消息标签、包含的数据项个数等,这是不是有点类似于HTTP响应包所包含的内容,原理相似

消息状态 Demo:

#include "mpi.h"
#include <iostream>
using namespace std;
void main(int argc, char ** argv)
{
	int rank;
	MPI_Status status;
	MPI_Init(&argc, &argv);
	MPI_Comm_rank(MPI_COMM_WORLD, &rank);
	if (rank == 0)
	{
		while (1)
		{
			int a;
			MPI_Recv(&a, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);
			cout << "recv " << a << " from p" << status.MPI_SOURCE << endl;
		}
	}
	else
	{
		int a = rank * rank;
		MPI_Send(&a, 1, MPI_INT, 0, rank, MPI_COMM_WORLD);
	}
	MPI_Finalize();
}

mpiexec 指定n=5,运行结果如下:(有4个进程Send,一个进程Recv)
在这里插入图片描述

点到点通信:单个进程对单个进程的通信

在这里插入图片描述

阻塞通信:

调用返回时要求通信操作已经正确完成,缓冲区可以使用

在阻塞情况下,当①完成,MPI_Send返回;当③完成,MPI_Recv返回。(回调函数)

阻塞Demo:

#include "mpi.h"
#include <iostream>
#include <windows.h>
using namespace std;
const int N = 2000;
void main(int argc, char ** argv)
{
	int a[N];
	int rank;
	MPI_Status status;
	MPI_Request request;
	MPI_Init(&argc, &argv);
	double t = MPI_Wtime();
	MPI_Comm_rank(MPI_COMM_WORLD, &rank);
	if (rank == 0)
	{
		for (int i = 0; i < N; i++)
			a[i] = 0;
		MPI_Send(a, N, MPI_INT, 1, 0, MPI_COMM_WORLD);
		for (int i = 0; i < N; i++)
			a[i] = 1;
		cout << "time of send is " << MPI_Wtime() - t << endl;
	}
	else if (rank == 1)
	{
		Sleep(2000);
		MPI_Recv(a, N, MPI_INT, 0, 0, MPI_COMM_WORLD, &status);
		cout << "time of recv is " << MPI_Wtime() - t << endl;
		for (int i = 0; i < N; i++)
			if (a[i] != 0)
			{
				cout << "error!" << endl;
				break;
			}
	}
	MPI_Finalize();
}

发送消息的进程和接收消息的进程基本同时结束,且程序没有报error错误。也就是可以得到以下结论:

  • MPI_Send和MPI_Recv都是阻塞的,只有当数据送出用户缓冲区或抵达用户缓冲区后,两个函数调用才会返回
非阻塞通信:
  • 不必等到通信操作完成便可以返回,通信操作可以交给底层的通信系统去完成
  • 调用返回并不保证资源的可再用性
  • 通过非阻塞通信将通信操作交给特定的通信硬件去完成
  • 在通信硬件完成通信操作的同时,处理器可以同时进行计算处理

非阻塞通信 Demo:

#include "mpi.h"
#include <iostream>
#include <windows.h>
using namespace std;
const int N = 2000;
void main(int argc, char ** argv)
{
	int a[N];
	int rank;
	MPI_Status status;
	MPI_Request request;
	MPI_Init(&argc, &argv);
	double t = MPI_Wtime();
	MPI_Comm_rank(MPI_COMM_WORLD, &rank);
	if (rank == 0)
	{
		for (int i = 0; i < N; i++)
			a[i] = 0;
		MPI_Isend(a, N, MPI_INT, 1, 0, MPI_COMM_WORLD, &request); // 改成MPI_Isend
		for (int i = 0; i < N; i++)
			a[i] = 1;
		cout << "time of send is " << MPI_Wtime() - t << endl;
	}
	else if (rank == 1)
	{
		Sleep(2000);
		MPI_Irecv(a, N, MPI_INT, 0, 0, MPI_COMM_WORLD, &request); // 改成MPI_Irecv
		cout << "time of recv is " << MPI_Wtime() - t << endl;
		for (int i = 0; i < N; i++)
			if (a[i] != 0)
			{
				cout << "error!" << endl;
				break;
			}
	}
	MPI_Finalize();
}

程序运行结果:
在这里插入图片描述
可以看到send一下子就返回了,但因为是非阻塞,因此后面的程序接着运行,将数组元素改成1,造成了数据修改。

因此,第二个进程2s后接收到的数据就不是send时候的快照。

点到点的通信死锁

在这里插入图片描述

死锁Demo:

#include "mpi.h"
#include <stdio.h>
const int n = 100000;
void main(int argc, char ** argv)
{
	int* a = new int[n];
	int* b = new int[n];
	int rank;
	MPI_Status status;
	MPI_Init(&argc, &argv);
	MPI_Comm_rank(MPI_COMM_WORLD, &rank);
	if (rank == 0)
	{
		for (int i = 0; i < n; i++)
			a[i] = i;
		MPI_Send(a, n, MPI_INT, 1, 0, MPI_COMM_WORLD);
		MPI_Recv(b, n, MPI_INT, 1, 0, MPI_COMM_WORLD, &status);
	}
	else if (rank == 1)
	{
		for (int i = 0; i < n; i++)
			b[i] = i * 2;
		MPI_Send(b, n, MPI_INT, 0, 0, MPI_COMM_WORLD);
		MPI_Recv(a, n, MPI_INT, 0, 0, MPI_COMM_WORLD, &status);
	}
	printf("P%d completed\n", rank);
	MPI_Finalize();
	delete[] a;
	delete[] b;
}

运行上面的程序,卡住无法继续往下,因为在阻塞条件下,进程A发送a数据,之后接收b数据;而进程B发送b数据,之后接收a数据;在数据没有被对方接收之前,发送都是无法继续往下执行的。操作系统课告诉我们,可以通过破坏以下任意一个条件:1. 互斥等待;2. 资源保持;3. 不剥夺抢占;4. 循环等待 来预防死锁,因此我们把代码部分修改成:

if (rank == 0)
{
	for (int i = 0; i < n; i++)
		a[i] = i;
	MPI_Send(a, n, MPI_INT, 1, 0, MPI_COMM_WORLD);
	MPI_Recv(b, n, MPI_INT, 1, 0, MPI_COMM_WORLD, &status);
}
else if (rank == 1)
{
	for (int i = 0; i < n; i++)
		b[i] = i * 2;
	MPI_Recv(a, n, MPI_INT, 0, 0, MPI_COMM_WORLD, &status);
	MPI_Send(b, n, MPI_INT, 0, 0, MPI_COMM_WORLD);
}

或者是通过非阻塞的方案来解决:

if (rank == 0)
{
	for (int i = 0; i < n; i++)
		a[i] = i;
	MPI_Isend(a, n, MPI_INT, 1, 0, MPI_COMM_WORLD, &request);
	MPI_Irecv(b, n, MPI_INT, 1, 0, MPI_COMM_WORLD, &request);
}
else if (rank == 1)
{
	for (int i = 0; i < n; i++)
		b[i] = i * 2;
	MPI_Isend(b, n, MPI_INT, 0, 0, MPI_COMM_WORLD, &request);
	MPI_Irecv(a, n, MPI_INT, 0, 0, MPI_COMM_WORLD, &request);
}

群集通信

特点
  • 一个进程组中的所有进程都参加的全局通信操作
  • 涉及的进程组以及通信上下文由通信域参数决定
  • 群集通信产生的消息不会和点对点通信产生的消息相混淆
  • 实现三个功能:
    通信:主要完成组内数据的传输
    聚集:在通信的基础上对给定的数据完成一定的操作
    同步:实现组内所有进程在特定的地点在执行进度上取得一致
  • 通信功能
    一对多通信
    多对一通信
    多对多通信
    Root进程
    • 在一对多通信中负责发送消息的进程
    • 在多对一通信中负责接收消息的进程
通信功能
广播

一对多通信,Root进程发送相同的消息给通信域Comm中的所有进程
在这里插入图片描述
广播Demo:

#include "mpi.h"
#include <stdio.h>
void main(int argc, char ** argv)
{
	int a[10];
	int rank;
	MPI_Init(&argc, &argv);
	MPI_Comm_rank(MPI_COMM_WORLD, &rank);
	if (rank == 0)
	{
		for (int i = 0; i < 10; i++)
			a[i] = i;
		MPI_Bcast(a, 10, MPI_INT, 0, MPI_COMM_WORLD);
	}
	else
	{
		// 对偶,注意这里不是写MPI_Brecv
		MPI_Bcast(a, 10, MPI_INT, 0, MPI_COMM_WORLD);
		printf("P%d: ", rank);
		for (int i = 0; i < 10; i++)
			printf("%d ", a[i]);
		printf("\n");
	}
	MPI_Finalize();
}

在这里插入图片描述

散播

也是一对多通信,Root进程给所有进程发送一个不同的消息,这些消息按进程标识的顺序有序地存放在其发送缓冲区中
在这里插入图片描述
散播Demo:

#include "mpi.h"
#include <stdio.h>
void main(int argc, char ** argv)
{
	int rank;
	MPI_Init(&argc, &argv);
	MPI_Comm_rank(MPI_COMM_WORLD, &rank);
	int b[10];
	if (rank == 0)
	{
		int a[40];
		for (int i = 0; i < 40; i++)
			a[i] = i;
		MPI_Scatter(a, 10, MPI_INT, b, 10, MPI_INT, 0, MPI_COMM_WORLD);
	}
	else
		MPI_Scatter(NULL, 10, MPI_INT, b, 10, MPI_INT, 0, MPI_COMM_WORLD);
	printf("P%d: ", rank);
	for (int i = 0; i < 10; i++)
		printf("%d ", b[i]);
	printf("\n");
	MPI_Finalize();
}

在这里插入图片描述

收集

多对一通信,Root进程从通信域Comm的所有进程接收消息,并按照进程的标识排序进行拼接,然后存放在其接收缓冲中
在这里插入图片描述
收集Demo:

#include "mpi.h"
#include <stdio.h>
void main(int argc, char ** argv)
{
	int rank;
	MPI_Init(&argc, &argv);
	MPI_Comm_rank(MPI_COMM_WORLD, &rank);
	int b[10];
	for (int i = 0; i < 10; i++)
		b[i] = i*rank;
	if (rank == 0)
	{
		int a[40];
		MPI_Gather(b, 10, MPI_INT, a, 10, MPI_INT, 0, MPI_COMM_WORLD);
		for (int i = 0; i < 40; i++)
			printf("%d ", a[i]);
	}
	else
		MPI_Gather(b, 10, MPI_INT, NULL, 10, MPI_INT, 0, MPI_COMM_WORLD);
	MPI_Finalize();
}

在这里插入图片描述

全局收集

多对多通信,每一个进程都收集来自所有进程的数据
在这里插入图片描述

全局交换

多对多通信,每个进程发送一个消息给所有进程,发送和接收的消息都分别按进程标识的顺序有序地存放在其发送和接收缓冲区中
在这里插入图片描述

同步和聚合功能
  • 同步:协调各个进程之间的进度和步伐,可调用 MPI_Barrier(Comm) 实现COMM域中所有进程相互同步,在该操作调用返回后,可以保证组内所有的进程都已经执行完了调用之前的所有操作
  • 聚合:使MPI在通信的同时完成一定的计算
    分三步实现:
    • 通信:根据要求发送消息到目标进程
    • 计算:执行计算功能。
    • 存储:把处理结果放入指定的接收缓冲区
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值