我的并行计算之路(三)MPI集合通信之Scatter和Gather

集合通信对比点对点通信来说,确实要方便许多,基本上不需要考虑各个进程该怎么发送,该怎么接收的问题,只要是使用一个通信子,函数接口自己就完成了。从进程的角度考虑,集合通信在分配任务量的时候更加平均,使各个进程的工作量更加相同,减少让一个进程死干活,其他进程在旁边看的情况。下面还是先上完整代码:

#include
#include
using namespace std;

const int maxn=10;

//计算x,y两个数组对应位的和,存入第三个数组z
bool Parallel_vector_sum(
				double local_x[],	//加数组
				double local_y[],	//被加数组
				double local_z[],	//存储和的数组
				int local_n			//加的个数
				)
{
  for(int local_i=0;local_i<local_n;local_i++){
  	local_z[local_i] = local_x[local_i]+local_y[local_i];
  }
  return true;
}

//从键盘读取数组
bool Read_vector(
			double local_a[],	//存储数据的数组,OUT
			int local_n,		//每个进程需要分配的数量
			int n,				//读取的总个数
			string vec_name,	//数组的名字
			int my_rank,		//进程号
			MPI_Comm comm		//通信子
			)
{
  double* a = NULL;

  if(my_rank==0){
	//输入数组
    a = new double[n];
    cout<<"Enter the vector "<<vec_name<<endl;
    for(int i=0;i<n;i++){
      cin>>a[i];
    }
	
	//数据分发
    MPI_Scatter(a,local_n,MPI_DOUBLE,local_a,local_n,MPI_DOUBLE,0,comm);
	
    delete [] a;
  } 
  else{
	//接收数据
    MPI_Scatter(a,local_n,MPI_DOUBLE,local_a,local_n,MPI_DOUBLE,0,comm);
  }
  return true;
}

bool Print_vector(
			double local_b[],	//需要输出的数组
			int local_n,		//每个进程需要输出的数量
			int n,				//总数量
			string title,		//数组的名字
			int my_rank,		//进程号
			MPI_Comm comm		//通信子
			)
{
  double* b=NULL;
  
  if(my_rank==0){
	//接收数组
    b = new double[n]; 
    MPI_Gather(local_b,local_n,MPI_DOUBLE,b,local_n,MPI_DOUBLE,0,comm);
	
	//打印数组
    cout<<title<<endl;
    for(int i=0;i<n;i++){
      cout<<b[i]<<" ";
    }
    cout<<endl;
    delete [] b;
  }
  else{
	//发送数组
    MPI_Gather(local_b,local_n,MPI_DOUBLE,b,local_n,MPI_DOUBLE,0,comm);
  }
  return true;
}

int main(void)
{
  int my_rank=0;
  int comm_sz=0;

  MPI_Init(NULL,NULL);
  MPI_Comm_rank(MPI_COMM_WORLD,&my_rank);
  MPI_Comm_size(MPI_COMM_WORLD,&comm_sz);

  streambuf * inbuf = cin.rdbuf((new ifstream("input"))->rdbuf());//重定向,OJ时将它注释掉

  int local_n=maxn/comm_sz;
  double local_a[maxn],local_b[maxn],local_c[maxn];
  
  //读取待相加的两个数组
  Read_vector(local_a,local_n,maxn,"arryA",my_rank,MPI_COMM_WORLD);
  Read_vector(local_b,local_n,maxn,"arryB",my_rank,MPI_COMM_WORLD);
  
  //计算对应的和
  Parallel_vector_sum(local_a,local_b,local_c,local_n);
  
  //打印结果数组
  Print_vector(local_c,local_n,maxn,"arry",my_rank,MPI_COMM_WORLD);


  MPI_Finalize();
  return 0;
}

首先介绍一下两个重要的函数接口MPI_Scatter()和MPI_Gather()。函数原型为:

int MPI_Scatter(
			void* 			send_buf_p,		/* IN */
			int 			send_count,		/* IN */
			MPI_Datatype 	send_type,		/* IN */
			void*			recv_buf_p,		/* OUT */
			int				recv_count,		/* IN */
			MPI_Datatype	recv_type,		/* IN */
			int				src_proc,		/* IN */
			MPI_Comm		comm			/* IN */
			)

MPI_Scatter()函数可以将分量发送给需要分量的其他进程,对于分发的进程src_proc而言,该函数用来分发数据;对于其他进程而言,该函数用来接收数据。其将send_buf_p指向的数据分成若干份,每份send_count个。然后将第一份交给0号进程,第二份交给1号进程…。在接收时,进程用本地的指针recv_buf_p来接收数据,接收数量为recv_count。一般情况下满足

  • send_count == recv_count
  • send_type == recv_type
  • 块划分方法
  • 通信子comm相同
  • 数据总数n能被进程数量comm_sz整除

下面是MPI_Gather()函数原型:

int MPI_Gather(
			void* 			send_buf_p,		/* IN */
			int 			send_count,		/* IN */
			MPI_Datatype 	send_type,		/* IN */
			void*			recv_buf_p,		/* OUT */
			int				recv_count,		/* IN */
			MPI_Datatype	recv_type,		/* IN */
			int				dest_proc,		/* IN */
			MPI_Comm		comm			/* IN */
			)

MPI_Scatter()函数可以将分量发送给需要分量的其他进程,对于收集数据的进程dest_proc而言,该函数用来收集数据;对于其他进程而言,该函数用来发送数据。接收0号进程数据作为第一份存入recv_buf_p的第一个块中,接收1号进程数据作为第二份存入recv_buf_p的第一个块中…,接收每个进程recv_count个数据。在发送时,进程用本地的指针send_buf_p来发送数据,发送数量为recv_count。一般情况下满足条件与分发相同:

  • send_count == recv_count
  • send_type == recv_type
  • 块划分方法
  • 通信子comm相同
  • 数据总数n能被进程数量comm_sz整除

然后讲解一下程序的思路。首先通过0号进程从键盘接收待加的a和b数组,然后将数据平均分发给各个进程。各个进程分别计算分发给自己的数据和,将对应的数据存入结果数组c中。然后调用打印函数,在0号进程将各个进程的结果拼接在一起,打印出来。
  最后补充一个广播函数MPI_Bcast,用来将数据广播到各个进程,原型为

int MPI_Bcast(
			void*			data_p,			/* in/out */
			int				count,			/* in */
			MPI_Datatype	datatype,		/* in */
			int				source_proc,	/* in */
			MPI_Comm		comm			/* in */
			)

data_p既作为输入数据也作为输出数据,此外需要指出传递的数据量和数据类型,还需要指定数据源的进程号。所以原来的get_input 函数(参见 我的并行计算之路(二) 中完整代码里的get_input 函数)就可以改写成:

//获取输入,0进程从键盘读取,非0进程从0进程获取
bool get_input(double &a,double &b,int &n)
{
    if(my_rank==0){	//用户输入
		cout<<" enter a,b,n"<<endl;
		cin>>a>>b>>n;
    }
	
	MPI_Bcast(&a, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
	MPI_Bcast(&b, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
	MPI_Bcast(&n, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
	
    return true;
}

可见,利用广播函数后,程序会变得简洁,而且不需要考虑是发送还是接受进程,不用填写复杂的参数了。
  
参考文献

[1] (美)Pacheco.An Introduction to Parallel Programming[M].邓倩妮等译.北京:机械工业出版社,2012.8

你可能感兴趣的:(并行计算)