1. 什么是进程、线程?
图:消息传递
现实世界中大量存在着信息传递的现象。如QQ、微 信的文字、图片和视频的发送与接收;再到本地端和互联网之间的网络请求发送和结果返回。
为了便于理解,我们先介绍几个概念。
1. 节点:服务器,等同于一台台式或者笔记本电脑。许多节点组成集群甚至是超算系统。
2. 进程:程序运行的实例对象,进程拥有独立的堆栈以及数据,数据不能共享。进程可以使用MPI进行跨节点通信。
3. 线程:是进程中的实际运作单位,被包含在进程之中。进程可以调用多个线程来处理任务,但线程不能开启进程。
线程内可以有独立的内存及数据,也可以线程间共享数据。
线程一般用于节点内并行,一般不用做跨节点运行。
3. 节点内 进程数*线程数<=节点核数
假如节点有24核,运行4个进程,每个进程最多开6个线程。超线程会导致程序运行很慢很慢。
更好的理解进程
图:windows资源管理器界面
以上是windows的资源管理器中的界面端。界面中现实的一个个运行实例就是进程。
更好的理解通信——以QQ为例
图:QQ中的消息传递
上图展示了2个QQ账号之间消息传递的过程:登录、知道对方QQ号、输入文字、点击发送、发送成功、退出QQ。
在这个过程中,两个QQ通过软件登录,生成2个独立的进程。每一个QQ内部都有自己的好友和设置,类似于进程之间的数据隔离。
2. MPI概述及程序编译运行
并行程序消息传递模型——MPI
MPI(Message Passing Interface),消息传递接口。主要用于进程间的消息传递(或者数据传递),主要发起者为2022年图灵奖获得者——Jack j. dongarra
1. MPI是一种新的库描述,不是一种语言。MPI共有上百个函数调用接口,提供与C和Fortran语言的绑定。
2. MPI是一种标准或规范的代表,而不是特指某一个对它的具体实现。迄今为止所有的并行计算机制造商提供对MPI的支持,具体有以下三个制造商:
Intel MPI
OpenMPI
mpich
3. MPI是一种消息传递编程模型,并成为这种编程模型的代表和事实上的标准。
MPI程序编译、运行
图:MPI程序编译、运行。
上图展示了Intel MPI、mpich和OpenMPI的使用的程序和编译器,以及超算上编译MPI的命令。
MPI程序运行——mpirun
在计算机上,我们使用mpirun来运行mpi程序(intel mpi、mpich、openmpi等)
用法:
mpirun -n 进程数 可执行文件名
示例:
mpirun -n 2 ./sampleC
这个运行过程被称作SPMD(Single Program Multi Data),即一个项目多份数据。将一个项目中的数据按照线程数分别运行。
注:如果在天河超算服务器上运行该代码严格上是不规范的,应该写作yhrun而不是mpirun。
图:并行程序示意图
但要注意,正是由于SPMD的特点。MPI在运行过程中会出现输出错误,也就是可能会一次输出多个数据。从操作系统的层面上说,运行程序再通过操作系统的IO缓冲区时,由于并行程序运行的时间接近,就会在屏幕上出现多个数据。
MPI程序——四个基本接口
图:MPI四个基本接口
上图展示的代码中,蓝色字体展示了MPI的四个接口。下面分别介绍四个接口的功能:
1. MPI_Init(&argc, &argv):初始化MPI环境,MPI将通过argc,argv得到命令行参数。
2.MPI_Comm_rank(MPI_COMM_WORLD,&myrank):缺省的通信域为MPI_COMM_WORLD,获取当前进程的进程ID,赋值给myrank。
3. MPI_Comm_size(MPI_COMM_WORLD, &size):获取缺省通信域总的进程数目,赋值给size。
4. MPI_Finalize():一般放在程序最后一行。如果没有此行,MPI程序将不会终止。
MPI程序——并行程序
1. 相并行(Phase Parallel)——对等模式
指的是每个节点间地位一致,执行操作完全一致。
图:相并行模式或对等模式
2. 主从并行(Master-Slaver Parallel)
拥有一个主进程
主进程一般作为计算任务的分配方
从进程执行计算任务
主进程负责管理数据的分发与接收
图:主从模式
3. 分治并行(Divide and Computer Parallel)
4. 流水线并行(Pipeline Parallel)
5. 工作池并行(Work Pool Parallel)
MPI通信详解
MPI通信——点对点通信
点对点通信:用于MPI中一个进程与另一个进程中传输数据。
唯一发送进程
唯一接收进程
图:点对点通信
常用的点对点通信有以下两种:
阻塞型:MPI_Send & MPI_Recv
非阻塞型:MPI_Isend & MPI_Irecv
点对点通信——阻塞式
图:阻塞式通信示意图
如上图所示,阻塞时通讯执行时,必须要接收成功或发送完成才能继续执行下一步。MPI_Send和MPI_Recv需要等待指定操作完成,或至少数据被MPI环境安全备份之后才会被返回。
点对点通信——MPI_Send
以下是函数格式:
MPI_Seng(buffer, count, datatype, destination, tag, communicator)
第一个参数指明消息缓存的起始地址,即存放要发送的数据信息。
第二个参数指明消息中给定的数据类型有多少项,数据类型由第三个参数给定。
第三个参数是数据类型,要么是基本数据类型,要么是导出数据类型,后者由用户生成指定一个可能是由混合数据类型组成的非连续数据项。
第四个参数是目的进程的标识符(进程编号)。
第五个是消息标签。
第六个是参数标识进程和通信上下文,即通信域。通常,消息只在同组的进程间传递。但是MPI允许通过intercommunicators在组间通信。
图:上图对比了MPI数据类型和C语言中的数据类型。
点对点通信——MPI_Recv
以下是函数格式:
MPI_Recv(address, count, datatype, source, communicator, status)
第一个参数指明接收信息缓冲的起始地址,即存放接受消息的内存地址。
第二个参数指明给定数据类型可以被接收的最大项数。
第三个数据指明接收的数据类型。
第四个参数是源进程标识符(编号)。
第五个是消息标签。
第六个参数标识一个通信域。
第七个参数是一个指针,指向一个结构:MPI_Status Status
存放有关接收消息的各种信息。(Status.MPI_SOURCE, Status.MPI_TAG)。
MPI_Get_count(&Status, MPI_INT, &C)读出实际接收到的数据项数。
下图则展示了阻塞性数据的示例程序
图:阻塞性数据
点对点通信——非阻塞式通信
图:非阻塞式通信示意图
如上图所示,非阻塞型通信的特点是能在等待消息到来的时候自由自在地进行其他操作。MPI_Isend, MPI_Irecv在调用后总是立即返回,实际操作由MPI在后台执行。为了确保通信完成,用户需要使用MPI_wait或MPI_Test等来等待或查询操作的完成情况。
下面将分别介绍4个函数:
1. int MPI_Isend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request)
IN buf:发送缓冲区的起始地址(可选数据类型)
IN count:发送数据的个数(整型)
IN datatype:发送数据的数据类型(句柄)
IN dest:目的进程号(整型)
IN tag:消息标志(整型)
IN comm:通信域(句柄)
OUT request:返回的非阻塞通信对象(句柄)
2. int MPI_Irecv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Request *request)
OUT buf:接收缓冲区的起始地址(可选数据类型)
IN count:接受数据的最大个数(整型)
IN datatype:每个数据的数据类型(句柄)
IN source:源进程标识(整型)
IN tag:消息标志(整型)
IN comm:通信域(句柄)
OUT request:非阻塞通信对象(句柄)
3. int MPI_Wait(MPI_Request *request, MPI_Status *status)
INOUT request 非阻塞通信对象(句柄)
OUT status 返回的状态(状态类型)
4. int MPI_Wait(MPI_Request *request, MPI_Status *status)
INOUT request:非阻塞通信对象(句柄)
OUT status:返回的状态(状态类型)
此过程表示阻塞直到非阻塞通信完成。
5. int MPI_Test(MPI_Request *request, int *flag, MPI_Status *status)
INOUT request:非阻塞通信对象(句柄)
OUT flag:操作是否完成标志(逻辑型)
OUT status:返回的状态(状态类型)
此过程表示检测非阻塞通信是否完成,不必等待。
非阻塞通信的完成与检测
图:非阻塞通信类型
之前我们提到非阻塞通信的4个常用函数,其中MPI_Test表示检测非阻塞通信是否完成,MPI_Wait表示阻塞直到非阻塞通信完成。上图展示了更多的非阻塞通信的检测函数。下面我们介绍MPI_Waitall函数,其余函数用户可自行查阅函数接口信息:
int MPI_Waita(int count, MPI_request *array_of_requests, MPI_Status *array_of_statuses)
int count:非阻塞通信对象的个数(整型)
INOUT array_of_requests:非阻塞通信完成对象数组(句柄数组)
OUT array_of_statuses:状态数组(状态数组类型)
下面展示了非阻塞型程序的代码:
图:非阻塞型通信程序示例
点对点通信——tag的使用
现在我们用一个例子来展示为什么要使用消息标签(Tag)?
图:使用与未使用信息标签进行传递的例子
如上图所示,代码需要传送A的前32个字节进入X,传送B的前16个字节进入Y。但是,如果消息B尽管先发送到到达进程Q,就会被第一个recv()接收在X中。使用信息标签就可以避免这个错误。
使用标签的另一个原因是可以简化对下列情形的处理:
假定有两个客户进程P和R,每个发送一个服务请求消息给服务进程Q,使用消息标签就可以轻松区分进程的接受方式:
图:两个客户进程P和R发送请求消息给服务进程Q
tag和source的特殊情况——MPI_ANY_TAG/MPI_ANY_SOURCE
MPI_ANY_TAG:如果给tag一个任意值MPI_ANY_TAG,则任何tag都是可接收的。
MPI_ANY_SOURCE:标识任何进程发送的消息都可以接收,即本接受操作可以匹配任何进程发送的消息,但其它的要求还必须满足,比如tag的匹配。
两者可以单独使用,也可以组合使用。