一幅长文总结并行计算——一幅长文系列
并行计算
作者:ArimaMisaki
|
目录
1 并行计算概述
1.1 基本概念
并行计算:并行计算机或分布式计算机(包括网络计算机)等高性能计算机系统上所做的超级计算
计算科学:用强大的计算能力去理解和解决复杂问题,是确保科学领先地位、经济竞争力和国防安全等的关键之所在。科学发现的第三支柱。
计算思维:运用计算机科学的基础概念进行问题求解、系统设计以及人类行为的理解,是21世纪中叶所有人的一种基本技能,就像现今人们掌握阅读、写作和算术技能一样,希望每个人都能像计算机科学家那样思考问题。
流水线:通常一条指令执行分为不同的阶段(如取指、译码、取数、执行等),通过重叠指令执行中的不同阶段,可以加快指令的执行速度。
超标量:如果CPU种设有多条流水线,即能同时发射多条指令,这种具有可在同一时钟周期内发射多条指令功能的处理器就成为超标量。
摩尔定律:“摩尔定律是由英特尔(Intel)创始人之一戈登·摩尔(Gordon Moore)提出来的。其内容为:当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。换言之,每一美元所能买到的电脑性能,将每隔18-24个月翻一倍以上。这一定律揭示了信息技术进步的速度。”
多核处理器:AMD和Intel在2005年相继推出了各自的双核处理器Opteron和Core Duo
拓展:多核处理器 多核CPU芯片可以看做是一个大芯片里面套了好几个小芯片,每个小芯片都可以看做是一个独立的CPU,对于Intel Xeon Phi(英特尔至强融核处理器)来说,其上面甚至集成了60多个核。 虽然多核CPU很牛,但是说到多核,可能没有比GPU更牛的了,GPU指的是成千上万个微核组成的处理器,其适用于大量的并行简单计算,还有图像处理,但是其不太适应串行任务,而且对于编程及算法的实现难度过高,所以一般操作系统还是运行在CPU上比较好。 |
1.2 存储器的层次结构
拓展:计算机中的存储器结构 这实际上是计算机组成原理的知识。 存储器一般分为多种,如主存、缓存、辅存、寄存器等等。这里图中出现了Cache。Cache一般指的是高速缓存,在以前Cache一般位于CPU之外,而在现在大多数Cache都在CPU内部。
Cache存储器(电脑中为高速缓冲存储器),是位于CPU和主存储器DRAM(Dynamic Random Access Memory)之间,规模较小,但速度很高的存储器,通常由SRAM(Static Random Access Memory)静态存储器组成。它是位于CPU与内存间的一种容量较小但速度很高的存储器。CPU的速度远高于内存,当CPU直接从内存中存取数据时要等待一定时间周期,而Cache则可以保存CPU刚用过或循环使用的一部分数据,如果CPU需要再次使用该部分数据时可从Cache中直接调用,这样就避免了重复存取数据,减少了CPU的等待时间,因而提高了系统的效率。Cache又分为L1Cache(一级缓存)和L2Cache(二级缓存),L1Cache主要是集成在CPU内部,而L2Cache集成在主板上或是CPU上。
主存一般指的是内存、寄存器,而辅存一般指外存(如磁盘、CD-ROM也就是光盘等)。各个存储器之间用总线进行通信,确保数据能够从计算机的一个位置传输到另外一个位置。
寄存器一般处于CPU内部,用来存放数据。对于常用的数据,一般先放在存储器,放不下了就放到高速缓冲去,再放不下就转移到磁盘。 |
1.3 并行计算
并行计算的初衷,是为了努力仿真自然世界中一个序列中含有众多同时发生的、复杂且相关事件的事务状态。
为了利用并行计算求解一个计算问题,通常基于以下考虑:
- 将计算任务分解成多个子任务,有助于同时解决
- 在同一个时间,由不同的执行部件可同时执行多个子任务
- 多计算资源下解决问题的耗时要少于单个计算资源下的耗时
并行计算基本上可以分为:
- 计算密集型
- 数据密集型
- 网络密集型
1.4 动态互连网络
动态网络是用交换开关构成的,可按应用程序的要求动态地改变连接组态;典型的动态网络包括总线、交叉开关和多级互连网络等。
这种网络比较普遍的是总线上面挂交换器。我们知道同一时间段中,一条总线只允许两头的设备进行信息交换,而在交换完成后,交换器可以将总线的端口改变,使其连接另外一个设备。通过这种方法,可以根据我们应用的需求,动态地选择我们需要的设备。
典型的动态网络区别如下:
拓展:并行计算机系统互连 不同带宽和距离的互连技术有多种,比较常用是:广域网WAN、城域网MAN、局域网WAN、个人区域网PAN、总线。广域网一般跨国,城域网一般城市,局域网一般一栋楼,个人区域网一般几台设备。其中广域网使用了交换技术,而局域网使用的是广播技术。如果是使用总线的话,总线是最快的,你可以理解为总结传输时不需要网络,直接用一条USB连接的那种。
静态互联网络是处理单元间有着固定连接的一类网络,在程序执行期间,这种点到点的连接保持不变;典型静态网络有一维线性阵列、二维网孔、树连接、超立方网络、立方环、洗牌交换网、蝶状网络等。 换而言之,静态互连网络就是用一个链路把多个处理器连接起来,构成物理意义上的并行计算机,如果某个处理器想发信息给另外一个处理器,总是能通过这条链路来干这种事。 相对地,我们还有动态互联网络。
互联网络中还有另外一个概念叫嵌入。其做法是将网络中的各节点映射到另一个网络中去。用膨胀系数来描述嵌入的质量,它是指被嵌入网络中的一条链路在所要嵌入的网络中对应所需的最大链路数。如果该系数为1,则称为完美嵌入。
对于环网和超立方来说,两者皆可被完美嵌入到2D环绕网中。 |
1.5 并行计算机结构模型
PVP
PVP也叫并行向量处理机(Parallel Vector Processor),其内部含有为数不多、功能强大的定制向量处理器,以及定制的高带宽纵横交叉开关和高速数据访问。其价格十分昂贵,因为其组件都需定制,一般适用于国家部门。
SMP
SMP也叫对称多处理机。其访存、IO都是对称的。其用的处理器大多数是商用处理器。
目前SMP需要解决的主要问题是Cache的一致性问题。多级高速缓存可以支持数据的局部性,而其一致性可由硬件来增强。大多数SMP系统都是基于总线连接的,占据了并行计算机市场中很大的份额。
MMP
MMP也叫`大规模并行处理机(Massively Parallel Processor)`,其规模大,性能好。
DSM
DSM又叫分布式共享存储器(Distributed Shared Memory,DSM)。在DSM中,每个节点都有本地内存,所有的节点都有一个共享空间。
COW
COW又叫工作站机群(Cluster of Workstation)。工作站机群的结构技术起点比较低,可以自己将一些服务器/微型机通过以太网连起来,加上相应的管理和通讯软件来搭建自己的工作站机群。
在集群中,每个节点都有本地磁盘,除了没有显示器没有鼠标没有键盘之外,基本上其他普通计算机该有的它都有。每个节点用I/O总线连向专门设计的多级高速网络。
机群也是构建并行计算机一种很廉价的方案,其被称为穷人的解决方案。使用这类并行计算机跑并行程序效率很低,但是由于它的性价比和搭建的简便性,使得近几年常被用于做并行科学计算和并行商用计算。
需要注意的是,机群不适合用于国家级的计算,因为由上述可知,实际上机群可以理解为是很多廉价的机器并在一起,而如果要运行速度跟快,能处理的数据更多,就需要并一个很大的机群。而如果机群并得很大,就会导致散热有问题。我们前面说过它们通过总线互联的,你总不能一个计算机在东一个计算机在西,然后一条总线连着吧。肯定是统一放在一个地方啊。而如果要处理大型的数据,一般机群所处的机房就要三四层楼那么高,篮球场那么宽,肯定不利于散热。
1.6 并行算法的基本设计策略
串行算法的直接并行化;如快排的自然并行化
从问题描述开始设计并行算法;如并行串匹配算法
借用已有算法求解新问题;如使用矩阵乘法算法求解所有点对间最短路径是一个很好的范例
并行算法常用设计技术
- 划分设计技术
- 分治设计技术
- 平衡树设计技术
- 倍增设计技术
- 流水线设计技术
注意:分治法和划分法的区别 分治法的侧重点在于子问题的归并上,而划分法的注意力则集中在原问题的划分上,分治是递归方式的划分,它将问题分成子问题后不立即求解它,而是连续地再将其分为更小的、易于求解的子问题。 |
1.7 并行编程风范
并行编程风范是指在并行机上编程实现并行算法的方法。如:
- 相并行
- 分治并行
- 流水线并行
- 主从并行
- 工作池并行
1.8 单核多线程和并发执行
并发执行是指多个线程在同一硬件资源上或单处理器核上交替地执行,在某个特定的时间点,所有活动的线程只有一个在真正执行,但在某段时间间隔内对外表现为多个线程在同时执行。
这种做法并非真正意义上的并行多线程,在单核结构上的应用程序主要靠隐藏延迟的方法来提高应用程序的性能。
影响多线程性能的常见问题有如下几点:
线程过多、数据竞争、死锁、Cache伪共享/Cache行乒乓现象。
注意:并行和并发 对于学过操作系统的都知道,比较容易混淆的就是并行的概念,我们所说的并行通常指的是:指两个或多个事件在同一时刻同时发生。 我们用一个例子来说明:有两个人一个叫小明一个叫小刚。它们每人都有两个女朋友。对于小明来说,他喜欢的是和一号、二号一起出门约会;而对于小刚来说,他喜欢8:00和一号约会,9:00和二号约会,10:00和一号约会。
这里我们发现两个人同样都是在约会,但是小明是同一时刻同时发生,属于并行;而小刚如果别人问他你怎么约会的,他会说他和两个女生同时约会,但是实际上,它是和两个女生交替约会,这就是宏观和微观的区别,其属于并发。
一个单核处理器(CPU)同一时刻只能执行一个程序,因此操作系统会负责协调多个程序交替执行,这就是操作系统的并发性。但是需要注意的是我们强调的是单核处理器,如今的电脑一般都是多核CPU,如Intel的第八代i3处理器就是4核CPU,这意味着同一时刻可以有4个程序并行执行,但是操作系统的并发性依然必不可少,因为每个人根本不可能说一台电脑只开四个应用程序吧。 |
1.9 拓展:并行计算机的分类
一台并行计算机可以是一台具有多个内部处理器的单计算机,也可以是多个互联的计算机构成一个一体的高性能计算平台。术语并行计算机通常是指专门设计的部件。根据不同的分类法可以分成不同类型的并行计算机。
1.9.1 费林分类法
在操作系统中我们知道,程序根据高级程序设计语言设计,程序设计语言在实现程序的功能的时候,是转换为机器指令来告诉机器该干什么。大概在50年前Flynn(1996)创造了一种计算机分类方法,中文译为费林分类法,该分类基于两个独立维度的计算机体系结构,这两个维度即数据和指令。根据以上提到这两个维度,我们可以划分为四大类,如图:
Single Instruction,Single Data(SISD)
SISD机器是一种传统的串行计算机,它的硬件不支持任何形式的并行计算,所有的指令都是串行执行。并且在某个时钟周期(时间片)内,CPU只能处理一个数据流。因此这种机器被称作单指令流单数据流计算机。早期的计算机都是SISD机器,如冯诺.依曼架构,如IBM PC机,早期的巨型机和许多8位的家用机等。
Multiple Instruction,Multiple Data(MIMD)
在一个通用的多处理机系统中,每个处理器拥有一个独立的程序,由每个程序为每个处理器生成一个指令流,不同的数据可能需要不同的处理,对应赋给不同的指令。每条指令对不同数据进行操作。Flynn将这种形式的计算机分类为多指令流多数据流计算机。
我们后面叙述的共享存储器或消息传递多处理机都属于MIMD类型。其已经经受了时间考验,至今仍然广泛地用于这种操作模式下的计算机系统中。例如多核CPU计算机。
Single Instruction,Multiple Data(SIMD)
如果对某些应用而言将计算机设计成由单一程序生成指令流,但是却有多个数据存在时,将会在性能上有很大的优势。打个比方,你输入一条指令就能够处理很多的数据,那不就是提高了性能吗。我们熟知的Hadoop就是基于SIMD的。
SIMD是采用一个指令流处理多个数据流。这类机器在数字信号处理、图像处理、以及多媒体信息处理等领域非常有效。
Intel处理器实现的MMXTM、SSE(Streaming SIMD Extensions)、SSE2及SSE3扩展指令集,都能在单个时钟周期内处理多个数据单元。也就是说我们现在用的单核计算机基本上都属于SIMD机器。
Multiple Instruction,Single Data(MISD)
MISD是采用多个指令流来处理单个数据流。由于实际情况中,采用多指令流处理多数据流才是更有效的方法,谁会去一个数据多个指令去处理呀。因此MISD只是作为理论模型出现,仅仅只在1971年CMU的实验中出现过,也就是说,实际上并不存在SISD。
1.9.2 存储器结构分类法
共享存储器多处理机
共享存储器多处理机可以理解为一个多核的计算机或者很多个单核的共用一份内存的计算机。当处理器想要处理数据,它就得跑去存储器拿数据。怎么知道数据在哪呢?通过存储器上的地址可以知道。
在操作系统中我们学过,如果这个时候两个处理器要同时在一个存储器上拿东西,那它们一定要提前沟通好,也就是说,两个处理器对共享空间的访问是互斥的。它们提前沟通的工具是互联网络。
多处理机系统由多台独立的处理机组成,每台处理机都能够独立执行自己的程序和指令流,相互之间通过专门的网络连接,实现数据的交换和通信,共同完成某项大的计算或处理任务。系统中的各台处理机由统一的操作系统进行管理,实现指令级以上并行,这种并行性一般是建立在程序段的基础上,也就是说,多处理机的并行是作业或任务级的并行。共享存储多处理机系统是指有一个可以被所有处理机访问的存储器系统。存储器系统由一个或多个存储器模块组成,所有的存储器模块使用一个统一的编址的地址空间。处理机可以用不同的地址访问不同的存储器模块。按存储器组织方式分类,共享存储多处理机系统分为集中式共享存储器系统和分布式共享存储器系统。
对共享存储器多处理机进行编程设计到在共享存储器中存有可由每个处理器执行的代码。每个程序所需的数据也将存于共享存储器中。(即程序段和数据段都在共享内存中)。因此如果有需要的话,每个程序可以访问所有的数据。
程序员要想使用并行计算机的每个处理器来处理一件问题,那原有的高级程序语言就无法使用了。所以为了解决此问题,程序员们开发了一种新的、高级并行程序设计语言,它具有特殊的并行程序设计构造和语句,以声明共享变量和并行代码段。虽然想法很好,但是这类并行程序设计语言并不是使用很广泛。
比较广泛的做法是在普通的高级程序语言的基础上生成并行代码,你可以理解为嵌入式编码(类似于嵌入式SQL)。此时使用制定好规则的编程语言,然后用预处理器命令对程序的并行部分加以说明即可;这类实践比较著名的模型就是OpenMP。它是由编译器命令和构造的一个工业标准,可融入到C/C++中。
另外,我们也可以多开几条线程,这样的话给人的感觉也像是并行计算的样子,不同线程中含有为各个处理器执行的规整的高级语言代码序列,这些代码序列可以用来访问共享单元。但是需要注意的是,实际上用线程的方法不是并行而是并发。
共享存储器多处理机是很一种很不错的并行计算机,综上所述,其方便了对数据的共享。
消息传递多计算机
多处理机系统的形式可以通过互联网络连接多台完整的计算机来构成。这实际上是使用了操作系统中的消息传递。
在消息传递多计算机中,一台计算机的处理器只能访问它对应本地的主存储器,而无法访问其他计算机上的主存储器。不同的计算机之间是用互联网来建立联系的,通常来说,多个电脑之间通过互联网传递的消息含有的可能是程序所指明的其他计算机处理器进行计算时所需的数据。这种多处理器系统我们通常称为消息传递多处理机(message-passing multiprocessor),或简称多计算机。你可以理解为多计算机实质上是真正意义上的分布式存储计算机。
我们在操作系统常提到进程这个概念,在多计算机上,我们可以把一个问题分成多个并发进程,它们可在各台计算机上分别执行。如果有6个进程和6个计算机,则我们可在每台计算机上执行一个进程;如果进程数大于计算机数,那么其中一台计算机中如果是多核可以采用并行执行,如果是单核可以采用分时方式执行。进程间将通过发送消息的原语来联系对方。同样地,发送消息可以采用两种方式,一种是直接通信方式,一种是间接通信方式,如果感兴趣可以去操作系统方面查找资料,这里不再细讲。
消息传递多计算机比共享存储器多处理机更容易在物理上进行扩展,也就是说它可以构成较大规模。一般规模比较小的叫做机群(Cluster),规模比较大的叫做超级计算机(SuperComputer),规模很大的叫做数据中心(DataCenter)。
1.10 并行层次和代码粒度
并行度:同时执行的分进程数
并行粒度:两次并行或交互操作之间所执行的计算负载
并行度与并行粒度大小常互为倒数:增大粒度会减小并行度
增加并行度会增加系统(同步)开销
按发送者数量和接受者数量参与通信可将发送分为:
- 一对一:点到点
- 一对多:广播、播散
- 多对一:收集、归约
- 多对多:全交换、扫描、置换/移位
延伸:粒度 在并行计算执行过程中,两个通信之间每个处理器计算工作量大小的粗略描述,分为细粒度和粗粒度。 粒度在并行算法设计中必不可少,通常在进程数与效率之间选择粒度的大小,比如后面将要介绍的MPI并行程序更适合粗粒度并行,而使用CUDA并行程序就需要细粒度。 |
1.11 并行程序设计模型
- 隐式并行:让编译器和运行时支持系统自动地开拓它
- 数据并行:并行操作于聚合数据结构(数组)
- 共享变量:驻留在各处理器上的进程可以通过读写公共存储器中的共享变量相互通信
- 消息传递驻留在不同处理器节点上的进程可以通过网络传递消息相互通信。
2并行计算模型
2.1 拓展:进程
在只有一个用户的PC机开机的时候,实际上会秘密启动很多进程。例如,启动一个进程用来等待进入的电子邮件;或者启动另一个防病毒进程周期性地检查是否有病毒库更新。或者更好笑的是,一开机就是垃圾捆绑软件,什么2345,什么网页游戏,这些都是进程。这么多进程的活动都是需要管理的,于是有一个支持多进程的多道程序系统在这里显得就很有用了。
在任何多道程序设计系统中,CPU能够很快地切换进程,这个很快是几百毫秒哦。这也就让人产生一种并行的错觉,在一秒钟内怎么开了这么多进程?同时开的吗?不是,实际上在一瞬间只能有一个进程让CPU服务,只是进程切换地太快了,这就是伪并行。这和真正意义上的并行是有区别的,这也导致了此情形可以用来作为判别是否为多处理器系统的指标。
2.2 拓展:进程模型
在操作系统中,进程模型简称进程,但实际上和进程有所区别。在进程模型中,计算机上所有可运行的软件,通常也包括操作系统,被组织成若干顺序进程,简称进程,进程是程序的一次执行过程。
每个进程都拥有自己的虚拟CPU,当然,实际上真正的CPU在各进程之间来回切换。在操作系统中时间复用技术曾经提到过,当一个资源在时间上复用时,不同的程序或用户轮流使用它。实际上对于CPU来说也是如此,在时间上进行复用的时候,不同的进程轮流使用它。这种快速地切换是需要特定的设计的,我们称为多道程序设计。
当然在上述的思考中,我们仅仅讨论的是单核CPU,而不是多核。如果是多核CPU,根据我们之前所说,多核CPU可以看成一个大CPU里面装了多个小的CPU;甚至于有的电脑还不止一个CPU,对于一些并行计算机,多处理器的情况也是很常见的。
拓展:时分复用技术和空分复用技术
这是操作系统系统四大特征——并发、共享、虚拟、异步中虚拟特征的两大技术。
2.3 拓展:父子进程
在Unix中,通过fork函数创建的新进程是原进程的子进程,而调用fork函数的进程是fork函数创建出来的新进程的父进程。也就是说,通过fork函数创建的新进程与原进程是父子关系,fork就相当于一个凭证,有fork,就有父子关系。
在Windows则没有这些说法,所有的进程地位都是相同的。
2.4 拓展:线程
在很久以前还没有引入进程之前,系统中的各个程序只能串行执行。比如你想要边听歌边开QQ,这是不可能做到的,只能先做一件事再做一件事。
后来引入进程后,系统中的各个程序可以并发执行。也就是说,可以同时听歌和开QQ。但是,即使引入了进程,也不能在QQ中同时视频聊天和传输文件。这是因为操作系统每一次执行都是按照进程为单位来执行的。
从上面的例子来看,进程是程序的一次执行。但是这些功能显然不可能是由一个程序顺序处理就能实现的。有的进程可能需要“同时做很多事”,而传统的进程只能串行地执行一系列程序。为此,引入了线程来提高并发度。
在传统中,进程是程序执行流的最小单位,也就是说,CPU每次执行任务,最少执行一个进程。而后在现在,CPU每次执行任务,最少执行一个线程,线程是进程的子集。也就是说,引入线程后,线程成为了程序执行流的最小单位。
需要知道的是,同个进程中所有线程的内存是共享的,如果是同个进程中的线程做通信交换数据非常快,但是不同进程的线程交换数据就很慢了。
2.5 拓展:用户线程和内核线程
用户级线程由应用程序通过线程库实现。所有的线程管理工作都由应用程序负责(包括线程切换)。用户级线程中,线程切换可以在用户态下即可完成,无需操作系统干预。在用户看来,是有多个线程;但是对于操作系统内核来说,并意识不到线程的存在。即用户级线程对用户不透明,对操作系统透明。
内核级线程(Kernel-Level Thread,KTL,又称为“内核支持的线程”)。内核级线程的管理工作由操作系统内核完成。线程调度、切换等工作都由内核负责,因此内核级线程的切换必然需要在核心态下才能完成。
2.6 POSIX线程
为了实现可移植的线程程序,IEEE定义了线程的标准。它定义的线程包叫做pthread,大部分UNIX系统支持该标准。这个标准定义了超过60个函数调用。常见的几个如下所示:
一言蔽之:Posix线程是一种标准,我们可以在任何编程语言中使用这个标准,如Java如果要开多线程就实现Thread这个类,这个类中的所有方法都是按照Posix这个标准制定的。
Posix线程模型具有如下特点:
- 可分为用户线程、内核线程和轻量级进程(LMP)
- 线程共享相同的内存空间
- 与标准fork()相比,线程带来的开销很小。内核无需单独复制进程的内存空间或文件描述符等等。这就节省了大量的CPU时间。
- 和进程一样,线程将利用多CPU。如果软件是针对多处理器系统设计的,则其为计算密集型应用。
- 支持内存共享无需使用繁杂的IPC和其他复杂的通信机制
2.7 并行算法
串行算法:解题方法的精确描述,是一组有穷的规则,它们规定了解决某一特定类型问题的一系列运算。
并行算法:一些可同时执行的诸进程的集合,这些进程互相作用和协调动作从而达到给定问题的求解。
描述语言:采用伪代码进行描述,在程序描述语言中引入并行语句
同步:在时间上强使各执行进程在某一点必须互相等待
通信:共享存储多处理器使用读写全局变量,分布存储多计算机使用发送和接收消息。
拓展:进程通信 在操作系统中,进程通信就是进程之间的信息交换。 进程是分配系统资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立。为了保证安全,一个进程不能直接访问另一个进程的地址空间,但是进程之间的信息交换又是必须实现的,为了保证进程之间的安全通信,操作系统提供了一些方法。
共享存储 使用共享存储的方式进行进程通信的话,操作系统会在内存中开辟一个共享空间,让两个进程进行通信。 需要注意的是:两个进程对共享空间的访问必须是互斥的(互斥访问通过操作系统提供的工具实现);并且操作系统只负责提供共享空间和同步互斥工具。
管道通信 管道是指用于连接读写进程的一个共享文件,又名pipe文件。其实就是在内存中开辟一个大小固定的缓冲区。需要知道的是: 1. 管道只能采用半双工通信,某一个时间段内只能实现单向的传输。如果要实现双向同时通信,则需要设置两个管道。 2. 各进程要互斥地访问管道 3. 数据以字符流的形式写入管道,当管道写满时,写进程的write()系统调用将被阻塞,等待读进程将数据取走。当读进程将数据全部取走后,管道变空,此时读进程的read()系统调用将被阻塞。 4. 如果没写满,就不允许读;如果没读空,就不允许写。 5. 数据一旦被读出,就从管道中被抛弃,这就意味着读进程最多只能有一个,否则可能会有读错数据的情况。
消息传递 进程间的数据交换以格式化的消息为单位。进程通过操作系统提供的“发送消息/接受消息”两个原语进行数据交换。 一个格式化的消息可以分为消息头和消息体。消息头包括:发送进程ID、接受进程ID、消息类型、消息长度等格式化的信息(计算机网络中发送的“报文”其实就是一种格式化的消息)。 消息传递也分为两种方式:
|
2.8 并行计算模型
- PRAM模型(Parallel Random Access Machine,并行随机存取机器):也称为共享存储的SIMD模型。其有一个假定的无限大的集中的共享存储器和一个指令控制器,通过SM的R/W交换数据,隐式同步计算。其中PRAM-CRCW(同时读同时写)是最强的计算模型,其隐藏了并行机的通讯、同步等细节。
- 异步APRAM模型
- BSP模型(Bulk Synchronous Parallel,大同步并行):一种分布存储的多计算机模型,计算是由一系列用全部同步分开的、周期为L的超级步所组成。在各超级步中,每个处理器均执行局部计算,并通过选路器接收和发送消息。
- LogP模型:一种分布存储的、点到点通信的多处理机模型。是比PRAM和BSP更一般的并行计算模型
2.9 并行算法一般设计过程
PCAM设计方法学
划分:分解成小任务,开拓并发性
通讯:确定诸任务间的数据交换,监测划分的合理性
组合:依据任务的局部性,组合成更大的任务
映射:将每个任务分配到处理器上,提高算法的性能(负载均衡)
一二阶段:考虑并发性、可扩放性,寻求具有这些特征的并行算法,即前期主要考虑如并发性等与机器无关的特性。
三四阶段: 将注意力放在局部性及其它与性能有关的特性上,即后期考虑与机器有关的特性。
2.10 程序性能评价与优化
并行执行时间=计算时间+并行开销时间+相互通信时间
存储器性能:估计存储器的带宽B
并行与通信开销的测量:乒乓方法
加速比性能定律
我们用p表示处理器数,用Wp表示使用具有p个处理器的多处理机的执行所需的时间,Ws表示使用单处理器系统执行时间。
Amdahl定律:固定负载的加速公式,为了归一化可将
看做。对加速公式求极限,当p趋近与无穷时,极限为。这表明了随着处理器数目的无限增多,并行系统所能达到的加速之上限为1/f。
Gustafson定律:。这表明随着处理器数目的增加,加速几乎与处理器数成比例的线性增加,串行比例f不再是程序的瓶颈。
3 OpenMP并行编程模型
3.1 OpenMP概述
OpenMP是由OpenMP Architecture Review Board牵头提出的,并已被广泛接受。其所支持的语言包括C、C++、Fortran。
OpenMP采用fork-join的执行模式,开始的时候只存在一个主线程,当需要进行并行计算的时候,派生出若干个分支线程来执行并行任务。当并行代码执行完成之后,分支线程会合,并把控制流程交给单独的主线程。
3.2 OpenMP语句模式
OpenMP通过编译指导命令来并行化,什么是编译指导命令?简单来说就是我们平常写的#开头的语句,通过程序中插入的这些编译指导命令,计算机就会完成并行计算的工作。在C/C++程序中,OpenMP的所有的编译指导命令都是以#pragma omp开始的,后面跟具体的功能指导命令,命令形式如下:
#pragma omp 指令 子句,子句,子句……
注意:由于我不太会C,所以这里使用C++。如果是第一次使用C++的话,可以简单理解为C++和C在以下代码中的不同仅限于输出是使用cout,而C使用printf。C++换行采用endl。且将要输出的东西由<<流向cout。
3.3 OpenMP简单演示
我们先从最简单的一个并行程序开始。在下面的代码中,我们只用parallel制导命令开启并行域,需要注意的是,如果不指定线程数的话默认启用与CPU核心数同等的线程数。
#include<omp.h> #include<iostream> using namespace std; int main() { #pragma omp parallel { cout << "Hello, world!" << endl; } } |
可以看出,从#pragma omp parallel开始的花括号内就是并行域。parallel制导命令表示接下来由花括号括起来的区域将创建多个线程并行执行。
我们还可以使用num_threads子句来控制线程的个数,需要注意的是,一般设置的线程数不超过CPU核心数,如下:
#include<omp.h> #include<iostream> using namespace std; int main() { omp_set_num_threads(2);//指定线程数为2 #pragma omp parallel { cout << "Hello, world!" << endl; } } |
我们可以使用制导命令for来提升for循环迭代的速度。并且可以使用omp_get_thread_num()查看对应任务在并行域中使用的线程号。在下面的代码演示中,我使用了for循环来循环4次,每次循环中打印本次循环使用的线程号。我指定了两条线程,线程号从0开始,说明任务只会使用0号线程或者1号线程。
#include<omp.h> #include<iostream> using namespace std; int main() { omp_set_num_threads(2); #pragma omp parallel { #pragma omp for for (int i = 0; i < 4; i++) cout << omp_get_thread_num() << endl; } } |
OpenMP实际上允许for写在parallel后面,即#pragma omp parallel for,不过这样写的坏处是会踩坑,所以平时建议不要这么写。
3.4 Schedule关于for循环的调度
在以上的演示中,我们发现任务是随机分配到各个线程上的,我们并没有做任何的调度。在下面的介绍中,我们使用schedule制导来进行for循环的调度。
schedule的基本形式是schedule(type, size),其中type参数有四种,分别是:1.static, 2.dynamic, 3.guided, 4.runtime,而size参数时整型数据,其表示循环迭代次数划分的单位。
static参数
static表示静态调度,这时候不用size参数,分配给每个程序的都是n/t次迭代,n为迭代次数,t为并行的线程数目。在下面的代码中,我指定了两条线程,且循环8次,则实际迭代次数只有4次。
#include<omp.h> #include<iostream> using namespace std; int main() { omp_set_num_threads(2); #pragma omp parallel for schedule(static) for (int i = 0; i < 8; i++) cout << omp_get_thread_num() << endl; } |
dynamic参数
动态调度模式是先到先得的方式进行任务分配,不用size参数的时候,先把任务干完的线程先取下一个任务,以此类推,而不是一开始就分配固定的任务数。使用size参数的时候,分配的任务以size为单位,一次性分配size个。虽然很智能,在任务难度不均衡的时候适合用dynamic,否则会引起过多的任务动态申请的开销。
guided参数
刚开始每个线程会分配到比较大的迭代块,后来分配到的迭代块逐渐递减,没有指定size就会降到1,否则降到size。
runtime参数
基本不会用到
3.5 设置环境变量(拓展)
这里设置环境变量你可以理解为在外面设置好的规则,程序内都必须遵从这个规则。常见的环境变量有:
OMP_SCHEDULE:用于for和parallel for中,决定了循环的各个迭代如何在处理中进行分配。
OMP_NUM_THREADS:定义执行中所能使用的最大线程数。
OMP_DYNAMIC:确定是否动态设定并行域执行部分的线程数
OMP_NESTED:确定是否允许嵌套并行
3.6 sections制导指令
用sections把不同的区域交给不同的线程去执行。在下面的代码中,我开启三条线程,并且使用section制导开启三块区域,每个区域由一个线程所负责。
#include<omp.h> #include<iostream> using namespace std; int main() { omp_set_num_threads(3); #pragma omp parallel sections { #pragma omp section { cout << omp_get_thread_num(); } #pragma omp section { cout << omp_get_thread_num(); } #pragma omp section { cout << omp_get_thread_num(); } } } |
3.7 single制导指令(拓展)
single制导指令所包含的代码段只有一个线程执行,别的线程跳过该代码,如果没有nowait子句,那么其他线程将会在single制导指令结束的隐式同步点等待,有nowait子句则其他线程将跳过等待往下执行。在下面的代码中,我开启四条线程,可以发现,只有一条线程服务于single制导命令下的代码段。
#include<omp.h> #include<iostream> using namespace std;
int main() { omp_set_num_threads(4); #pragma omp parallel { #pragma omp single { cout << "single thread=" << omp_get_thread_num() << endl; } cout << omp_get_thread_num() << endl; } } |
3.8 共享任务结构
- 共享任务结构将它所包含的代码划分给线程组的各成员执行
- 不产生新的线程
- 在共享任务结构的入口点没有路障
- 在其结束处有一个隐含的路障
- DO/FOR是最常用的循环,并且有SCHEDULE选项,可以指定采用何种调度算法
- SECTIONS可以让并行任务流水线执行(详见6)
- SINGLE只有一个处理机执行之(详见7)
3.9 OpenMP的优点和缺点
优点
- 提供了一个可用的编程标准
- 可移植,简单,可扩展
- 灵活支持多线程,具有负载均衡的潜在能力
缺点
- 只适用于硬件共享存储型的机器
- 动态可变的线程数使得支持起来困难
3.10 常用子句的补充
private:指定每个线程都有它自己的变量私有副本
firstprivate:指定每个线程都有它自己的变量私有副本,并且变量要被继承主线程中的初值。
lastprivate:主要是用来指定将线程中的私有变量的值在并行处理结束后复制回主线程中的对应变量。
nowait:忽略指定中暗含的等待
num_threads:指定线程的个数
schedule:指定如何调度for循环迭代
shared:指定一个或多个变量为多个线程间的共享变量
ordered:用来指定for循环的执行要按顺序执行
copyprivate:用于single指令中的指定变量为多个线程的共享变量
copyin:用来指定一个threadprivate的变量的值要用主线程的值进行初始化。
default:用来指定并行处理区域内的变量的使用方式,缺省是shared。
4 MPI并行编程模型
4.1 拓展:什么是MPI
MPI是一个跨语言的通讯协议,用于编写并行计算机。支持点对点和广播。MPI的目标是高性能,大规模性,和可移植性。MPI在今天仍为高性能计算的主要模型。
主要的MPI-1模型不包括共享内存概念,MPI-2只有有限的分布共享内存概念。 但是MPI程序经常在共享内存的机器上运行。在MPI模型周边设计程序比在NUMA架构下设计要好因为MPI鼓励内存本地化。
MPI是一个在平行计算中传递消息的库的标准,由实现人员和使用人员来遵守。目前的实现版本有MPICH2, Argonne National Laboratory实现,他还有好几个派生子项目。
4.2 MPI基本函数
- MPI_Init(…);
- MPI_Comm_size(…);
- MPI_Comm_rank(…);
- MPI_Send(…);
- MPI_Recv(…);
- MPI_Finalize();
int MPI_Init(int* argc,char** argv[])
- 用于并行环境初始化,其后面的代码到MPI_Finalize()函数之前的代码段都会在每个进程中(并行环境)执行一次。
- 除了MPI_Initialized()外,其余所有的MPI函数应该在其后才被调用
- MPI系统将通过argc,argv得到命令行参数,也就是说main函数必须带参数,否则会报错。
int MPI_Finalize (void)
- 退出MPI系统, 所有进程正常退出都必须调用。 表明并行代码的结束,结束除主进程外其它进程。
- 串行代码仍可在主进程(rank = 0)上运行, 但不能再有MPI函数(包括MPI_Init())。
int MPI_Comm_size (MPI_Comm comm ,int* size )
- 获得进程个数 size。
- 指定一个通信子,也指定了一组共享该空间的进程, 这些进程组成该通信子的group(组)。
- 获得通信子comm中规定的group包含的进程的数量。
int MPI_Comm_rank (MPI_Comm comm ,int* rank)
得到本进程在通信空间中的rank值,即在组中的逻辑编号(该 rank值为0到p-1间的整数,相当于进程的ID。)
int MPI_Send( void *buff, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)
- void *buff:你要发送的变量。
- int count:你发送的消息的个数(注意:不是长度,例如你要发送一个int整数,这里就填写1,如要是发送“hello”字符串,这里就填写6(C语言中字符串末有一个结束符,需要多一位))。
- MPI_Datatype datatype:你要发送的数据类型,这里需要用MPI定义的数据类型,可在网上找到,在此不再罗列。
- int dest:目的地进程号,你要发送给哪个进程,就填写目的进程的进程号。
- int tag:消息标签,接收方需要有相同的消息标签才能接收该消息。
- MPI_Comm comm:通讯域。表示你要向哪个组发送消息。
int MPI_Recv( void *buff, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status)
- void *buff:你接收到的消息要保存到哪个变量里。
- int count:你接收消息的消息的个数(注意:不是长度,例如你要发送一个int整数,这里就填写1,如要是发送“hello”字符串,这里就填写6(C语言中字符串末有一个结束符,需要多一位))。它是接收数据长度的上界. 具体接收到的数据长度可通过调用MPI_Get_count 函数得到。
- MPI_Datatype datatype:你要接收的数据类型,这里需要用MPI定义的数据类型,可在网上找到,在此不再罗列。
- int dest:接收端进程号,你要需要哪个进程接收消息就填写接收进程的进程号。
- int tag:消息标签,需要与发送方的tag值相同的消息标签才能接收该消息。
- MPI_Comm comm:通讯域。
- MPI_Status *status:消息状态。接收函数返回时,将在这个参数指示的变量中存放实际接收消息的状态信息,包括消息的源进程标识,消息标签,包含的数据项个数等。
4.3 消息传递的特点
在消息传递模型中,一个并行应用由一组进程组成,每个进程的代码是本地的,只能访问私有数据,进程之间通过传递消息实现数据共享和进程同步。
优点:用户可以对并行性的开发、数据分布和通信实现完全控制。
缺点:
- 要求程序员显式地处理通信问题,如:消息传递调用的位置,数据移动,数据复制,数据操作,数据的一致性等等。
- 对大多数科学计算程序来说,消息传递模型的真正困难还在于显式的域分解。也就是说,将对相应数据的操作限定在指定的处理器上进行。在处理器上只能看见整个分布数据的一部分。
- 无法以渐进的方式、通过逐步将串行代码转换成并行代码而开发出来,大量的散布在程序各处的域分解要求整个程序由串行到并行的转换一次性实现,这是消息传递的一个明显的缺点。
- 点赞
- 收藏
- 关注作者
评论(0)