并行程序设计方法和模型研究.docx

上传人:b****5 文档编号:6687515 上传时间:2023-01-09 格式:DOCX 页数:26 大小:470.52KB
下载 相关 举报
并行程序设计方法和模型研究.docx_第1页
第1页 / 共26页
并行程序设计方法和模型研究.docx_第2页
第2页 / 共26页
并行程序设计方法和模型研究.docx_第3页
第3页 / 共26页
并行程序设计方法和模型研究.docx_第4页
第4页 / 共26页
并行程序设计方法和模型研究.docx_第5页
第5页 / 共26页
点击查看更多>>
下载资源
资源描述

并行程序设计方法和模型研究.docx

《并行程序设计方法和模型研究.docx》由会员分享,可在线阅读,更多相关《并行程序设计方法和模型研究.docx(26页珍藏版)》请在冰豆网上搜索。

并行程序设计方法和模型研究.docx

并行程序设计方法和模型研究

并行程序设计方法和模型研究

摘要

随着并行硬件系统的发展,并行编程愈趋重要。

本文主要研究了并行程序的模型、设计方法以及并行编程的一些标准。

另外,随着通用GPU(GPGPU)的发展,本文还讨论了利用GPU进行并行计算,其中重点讲述了CUDA(通用并行计算架构)。

1.并行系统划分

要实现软件并行执行的目标,就必须为多个线程同时执行提供一个硬件平台。

对计算机体系结构分类的经典方法就是费林分类法。

费林分类法(Flynn'sTaxonomy)是一种高效能计算机的分类方式,1972提出,把计算机分为四种类型:

单指令流单数据流(SISD)、单指令流多数据流(SIMD)、多指令流单数据流(MISD)、多指令流多数据流(MIMD)。

SISD(SingleInstructionSingleData):

单处理指令单数据,一条指令处理一个数据,所有的冯诺依曼体系结构的“单处理器计算机”都是这类,其硬件不支持任何形式的并行,所有的指令都串行执行。

在某个时钟周期内,CPU只能处理一个数据流。

SIMD(SingleInstructionMultipleData):

单指令多数据,用一个指令流同时处理多个数据流。

此类机器在诸如数字信号处理、多媒体信息处理、向量运算处理等应用领域非常有效。

时至今日,几乎所有的计算机都以各种形式的指令集形式实现SIMD功能,例如Intel处理器中实现的MMX、SSE、SSE2、SSE3等扩展指令集。

这些扩展指令集都能够在单个时钟周期内处理多个数据单元,这些数据单元存储在浮点寄存器中。

MISD(MultipleInstructionSingleData):

多指令单数据,采用多个指令流同时对一个数据流进行处理。

但是在大多数情况下,多个指令流处理多个数据流才是更加有效的处理方式,因此,MISD并行计算机一般只是作为一种理论模型出现。

MIMD(MultipleInstructionMultipleData):

多指令多数据,每个处理单元有独立指令和数据。

能够同时执行多个指令流,这些指令流分别对不同的数据流进行操作。

MIMD是目前最流行的并行计算平台,现代流行的并行处理结构都可以划入到这一类。

1.1MIMD型计算机体系结构

MIMD性计算机体系结构细分的标准是内存结构,即内存是如何组织的。

内存结构可以简单的分为两大类:

共享内存和消息驱动。

共享内存就是指所有处理器共享内存,所有CPU由一个操作系统控制,通过共享内存进行通信;

消息驱动就是处理器之间不共享内存,靠消息驱动来进行通信,就是分布式内存,CPU由不同的操作系统控制,不同的CPU之间通过网络通信。

1.1.1共享内存型

共享内存型体系结构可以分为对称多处理机SMP,非一致内存访问NUMA。

对称多处理机SMP(symmetricmultiprocessors):

所有CPU都共享同一内存。

图1-1对称多处理机内存模式

目前Intel和AMD推出的多核CPU应该都划归到对称多处理机这一类。

非一致内存访问NUMA(nonuniformmemoryaccess):

所有CPU共享所有的内存,但不同的CPU访问不同的内存时速度不一样。

图1-2非一致内存访问内存模式

CPU访问离自己近的内存是本地直接访问的,访问离自己远的内存是通过总线访问的,因此两者速度差别很大,这也是之所以叫做非一致内存访问的原因。

与非一致内存访问NUMA类似的还有一个叫ccNUMA,前缀cc是cache-coherent(一致性高速缓存)的缩写,它其实就是为了解决访问不同内存速度不一样的问题。

1.1.2消息驱动型

消息驱动型体系结构比较典型的就是分布式内存(DM)结构。

分布式内存DM(Distributedmemory):

每个CPU都有自己的内存,CPU之间通过消息来通信。

图1-3分布式内存结构

根据interconnectnetwork的不同,分布式内存可分为三种:

大规模并行处理系统(MPP)、集群(Cluster)、网格系统(Grid)。

三者的差别简单的来说就是:

MPP是一台机器,Cluster是一群相同或者类似的机器,Grid是一堆任意的机器。

大规模并行处理系统MPP(massivelyparallelprocessors):

这样的系统是由许多松耦合的处理单元组成的,每个处理单元内的CPU都有自己私有的资源,如总线,内存,硬盘等。

在每个单元内都有操作系统和管理数据库的实例复本。

这种结构最大的特点在于不共享资源。

集群Cluster:

集群应该是大家最常见的,就是一堆相同或者类似的机器通过网络连起来组成一个计算机群,相互之间通过网络进行通信。

网格系统Grid:

网格系统其实和集群差不多,都是一堆机器通过网络连接起来,但两者还是有不同的地方:

1)集群中机器都是同质的,而网格中是异质的,可能一个大型机、一台PC机、一台手机都是网格系统中的一个机器;2)集群中的机器都是属于某一个实体的,需要“集中管理”,例如一个公司、一个组织、一个人;而网格中的机器属于不同的实体的,只需要“分散管理”即可,甚至大家都不知道有哪些机器在这个网格中。

3)大部分情况下,集群是通过局域网进行连接,网格是通过互联网连接。

2.并行编程模式

Ø并发与并行的区别:

并发是指多个线程在某段时间内能够同时被执行,并发可以在串行处理器上通过交错执行的方式来实现,但是在同一时刻,只能有一个活动线程;

并行是指多个线程在任何时间点都同时执行。

图2-1在单核平台上的线程并发

图2-2在多核平台上的线程并行

Ø并行可以分为数据并行和任务并行:

数据并行非常简单,它是指您有大量数据需要处理——例如图像中的像素,大量工资支票等。

把这些数据进行分解并交给多个核进行处理,这就是一种数据并行方法。

任务并行性则是指有多个任务需要完成。

例如求一个数据集的最小值、最大值和平均值,就可以让不同的核针对同一个数据集分别计算其最大、最小和平均值的答案。

从并行的层次上看,还可以把并行分为指令级并行和线程级并行。

指令级并行是指在单个CPU中,利用超标量、超级流水线、超长指令字、超线程、分支预测等方法,使得多条指令在同一个CPU上同时运行,以提高系统的吞吐率。

线程级并行是指在多核结构中,将多个线程分别分配到不同的核上执行。

指令级并行和线程级并行的一个很重要的区别在于,指令级并行对于程序员来讲是透明的,相反,线程级并行则需要程序员自己来分配执行单元,受到程序员的控制,因而更加灵活。

超标量:

通过在CPU上内置多条流水线,使CPU在一个时钟周期内可以执行多条指令,以空间换取时间。

超级流水线:

也叫深度流水线。

通过细化流水、提高主频,使得在一个周期内完成一个或多个操作,以时间换取空间。

超长指令字:

这里的指令是由编译器提取出来的可并行的若干指令组成的长指令,一条长指令用来实现多个操作的并行。

这减少了内存访问,提高了执行单元的利用率。

超线程:

超线程技术就是利用特殊的硬件指令,把两个逻辑内核模拟成两个物理芯片,减少了CPU的闲置时间,提高的CPU的运行效率。

虽然采用超线程技术能同时执行两个线程,但它并不像两个真正的CPU那样,每个CPU都具有独立的资源。

当两个线程都同时需要某一个资源时,其中一个要暂时停止,并让出资源,直到这些资源闲置后才能继续。

Ø并行计算的功能:

降低单个问题求解的时间;增加问题求解规模、提高问题求解精度、(多机同时执行多个串行程序)容错、更高的可用性、提高吞吐率。

Ø并行程序设计需要注意的四个方面:

同步、通信、负载平衡、可扩展性。

同步(Synchronization):

指两个或者多个线程协调各自行为的过程。

例如一个线程停下来等待另一个线程完成某项任务。

通信(Communication):

指线程之间交换数据相关的带宽和延迟问题。

负载平衡(LoadBalancing):

指多个线程之间工作量分布的情况。

负载平衡能够使各线程的工作量大致平均分配。

可扩展性(Scalability):

是衡量在性能更强劲的系统上运行软件时能够有效利用更多线程的指标。

例如,如果一个应用程序是面向四核系统编写的,那么当该程序在八核系统上运行时,其性能是否能够线性增长。

Ø并行程序设计方法:

隐式并行程序设计与显式并行程序设计。

隐式并行程序设计:

常用传统的语言编程成顺序源编码,把“并行”交给编译器实现自动并行。

程序的自动并行化是一个理想目标,存在难以克服的困难。

隐士并行程序设计的特点是语言容易,编译器难。

显式并行程序设计:

在用户程序中出现“并行”的调度语句。

显式的并行程序开发则是解决并行程序开发困难的切实可行的方法。

特点是语言难,编译器容易。

Ø并行程序设计模型:

隐式并行(ImplicitParallel)、数据并行(DataParallel)、共享变量(SharedVariable)、消息传递(MessagePassing)。

隐式并行(ImplicitParallel):

程序员用熟悉的串行语言编程(未作明确的制定并行性),编译器和运行支持系统自动转化为并行代码。

特点:

语义简单、可移植性好、易于调试和验证正确性、细粒度并行、效率很低。

数据并行(DataParallel):

类似于SIMD的自然模型。

特点:

单线程、并行操作于聚合数据结构(数组)、松散同步、单一地址空间、隐式交互作用、显式数据分布;优点:

编程相对简单,串并行程序一致。

缺点:

程序的性能在很大程度上依赖于所用的编译系统及用户对编译系统的了解。

并行粒度局限于数据级并行,粒度较小。

共享变量(SharedVariable):

对称多处理机的自然模型。

特点:

多线程、异步、单一地址空间、显式同步、隐式数据分布、隐式通信。

消息传递(MessagePassing):

集群的自然模型。

特点:

多线程、异步、多地址空间、显式同步、显式数据映射和负载分配、显式通信。

Ø分解

要采用并行程序设计模式来设计应用程序,设计人员应该将应用程序中能够并行执行的部分识别出来。

要做到这样,程序员必须将应用程序看做是众多相互依赖关系任务集合。

将应用程序划分成多个独立的任务,并确定这些任务之间的相互关系的过程称为分解(Decomposition)。

主要的分解方式有三种:

任务分解、数据分解、数据流分解。

表2-1分解方式

任务分解:

对应用程序根据其执行功能进行分解的过程称为任务分解,使独立任务能够同时运行。

如果两个任务能够同时运行,则对其进行调度,形成二者之间的并发执行。

一般而言,需要对分解出来的任务进行修改,避免各个任务之间的冲突,同时标记不再是串行执行的。

以园艺工作举例:

如果两个园丁到达一个客户家,一个修剪草坪,另一个铲除杂草。

修剪草坪和铲除杂草是两个被分开的功能。

要完成这两个功能,园丁们需要确保他们之间相互协调,这样铲除杂草的园丁就不会坐在待修剪草坪的中间。

数据分解:

也称为数据级并行(data-levelparallelism)。

将应用程序根据各任务所处理的数据而非按照任务的天然特性来进行分解的方法。

一般而言,能够按照数据分解方式进行分解的应用程序都包含多个线程,这些线程分别对不同的数据对象执行相同的操作。

数据分解所能够处理的问题规模随着处理器核数量的增长而增长。

如果园丁应用数据分解来分解他们的任务,他们两个会同时修剪一半的草坪,然后两个人分别铲除一半的杂草。

数据流分解:

更关心数据在各个任务间是如何流动的。

典型的是生产者/消费者模型,一个任务的输出是另一个任务的输入。

许多时候,当分解一个问题时,关键不是任务应该做什么事情,而是数据在不同任务中怎样传递。

在这些情况下,数据流分解将问题按数据在任务中传递的方式来分解。

生产者与消费者两个任务被不同的线程执行,直到生产者完成他的部分工作,消费者不能开始工作。

依然引用园丁的例子,一个园丁准备工具,譬如他承担为割草机加油,清扫剪刀等类似的任务来提供这些工具给另两个园丁使用。

直到这个准备步骤基本结束,其他园丁的园艺工作才能开始。

由第一个任务引起的延迟为第二个任务产生一个暂停,在此之后两个任务才能并行运行。

在计算机领域这样的模式经常发生。

生产者/消费者问题有许多需要注意的方面:

1)如果这种模式没有正确地执行,消费者和生产者间产生的依赖性会引起重大的延迟。

一个性能敏感的设计需要充分理解依赖关系的性质以减小延迟的影响。

这也是为了避免消费者线程空闲等待生产者线程的情况。

2)在完美的情况下,生产者和消费者间的传递是完全"清洁"的,消费者不需要知道生产者的任何事情。

然而在很多时候,生产者和消费者并不享受如此干净的任务分割,安排他们间的互动需要非常仔细的计划。

3)如果当生产者完全做好后消费者开始加工,那么当其他线程忙着工作时一个线程就保持空闲。

这个问题破坏了一个并行处理的重要目标,那就是负载均衡以使得所有能用的线程保持忙碌。

由于线程间的逻辑关系,要保持线程平等地被占用非常困难。

3.并行编程标准

对于共享内存型结构,可以通过操作系统的多线程来完成并行任务,通过线程间通信来完成协作。

对于消息驱动型结构,可以通过多台机器来完成并行任务,通过消息来完成协作。

消息的定义:

消息是一种将信息或信号从一个域传送到另一个域的特殊通信手段。

从消息共享的角度讲,实现消息共享有进程内传递、进程间传递和进程对进程传递。

当进行消息传递的两个线程处于同一进程内时,使用进程内消息传递模式;当进行消息传递的两个线程分属不同的进程时,使用进程间消息传递模式。

而从开发人员的角度来看,最普遍的消息传递方式应是进程对进程消息传递,即两个进程不依赖于线程自行进行通信。

采用共享存储模型的系统,消息传递是同步的,采用分布式存储模型的系统,消息传递时异步的。

消息传递必须通过一定的接口才能进行。

最常见的接口是MPI(MessagePassingInterface,消息传递接口)。

 

表3-1并行编程的标准

并行编程标准

典型

并行模式

线程库标准

Win32API

Posixthreads

共享变量

编译制导

OpenMP

IntelTBB

共享变量

消息传递库标准

MPI

PVM

消息传递

3.1Win32API与Posixthreads

3.3.1Windows线程库

过去开发Windows应用程序一般采用微软的Win32或MFCAPI并使用C/C++程序设计语言编写。

现在,许多新的应用程序主要采用微软的.NET平台和相关的通用语言运行时环境(CommonLanguageRuntime,CLR)进行开发。

Win32API是Windows操作系统为内核以及应用程序之间提供的接口,将内核提供的功能进行函数封装,应用程序通过调用相关的函数获得相应的系统功能。

MFC是微软基础函数类库(MicrosoftFoundationClasses),由微软提供的,用类库的方式将Win32API进行封装,以类的方式提供给开发者。

.NET由两部分构成:

公共语言运行库(CommonLanguageRuntime,CLR)和Framework类库(FrameworkClassLibrary,FCL)。

.NET基础类库的System.Threading命名空间提供了大量的类和接口来支持多线程。

所有与多线程机制相关的类都存放在System.Threading命名空间中。

Win32对多线程的支持的功能:

创建线程、线程终止、使用Windows事件进行线程通信、线程同步、线程优先级、处理器亲和。

3.3.2POSIXthreads线程模式

POSIX多线程,或者成为Pthreads,是一个可移植的多线程库,在设计上提供了在多个操作系统平台上使用一致的程序设计接口的功能。

Pthreads已成为Linux操作系统中多线程接口的标准,并且也已广泛使用在大多数的UNIX平台上。

针对Windows操作系统,Pthreads也存在一个开放源代码的版本,称为pthreads-win32。

如果打算使用C语言开发多线程程序,并且需要一个能比OpenMP提供更多直接控制的可移植的多线程API,那么Pthreads是一个不错的选择。

Pthread的主要功能包括线程的创建、线程的终止、线程同步、条件变量与信号量。

而类似于线程优先级这样的功能并不包括在核心Pthreads库中,而是作为可选功能的一部分由制造商实现。

图3-1Win32与Pthreads的函数比较

3.2OpenMP

OpenMP标准形成于1997年,它是一种API,它用于编写可移植的多线程应用程序。

支持C/C++/Fortran编程语言,支持Windows平台与linux平台。

OpenMP程序设计模型提供了一组与平台无关的编译指导(pragmas)、指导命令(directive)、函数调用和环境变量,可以显示地指导编译器如何以及何时利用应用程序中的并行性。

开发人员不需要关心那些实质性的实现细节,这是编译器和OpenMP线程库的工作,开发人员只需要认真考虑哪些循环应该以多线程方式执行,以及如何重构算法以便在多核处理器上获得更好的性能等问题。

实例:

#pragmaompparallelfor

for(i=0;i

{

#pragmaompparallelfor

for(j=0;j

{

inty=i/2;

intx=j/2;

RGBdata[i*width+j]=YUV_RGB32_float(Y+i*width+j,U+y*width/2+x,V+y*width/2+x);

}

}

OpenMP的优点:

多线程编程简单、移植性好、能够根据目标系统自动使用适当数量的线程、负载自动均衡。

OpenMP的缺点:

OpenMP不适合需要复杂的线程间同步和互斥的场合。

OpenMP的另一个缺点是不能在非共享内存系统(如计算机集群)上使用。

在这样的系统上,MPI使用较多。

3.3MPI

MPI(MessagePassingInterface)是一个消息传递接口的标准,1992年提出,用于开发基于消息传递的并行程序,其目的是为了提供一个实际可用的、可移植的、高效的和灵活的消息传递接口标准。

MPI支持C/C++和Fortran语言,支持Windows与linux操作系统。

MPI是一个库,它本身不能自编译,需要其它编程语言来调用;MPI是一种标准或规范的代表,而不是特指某一个对它的具体实现。

MPI标准定义了一组具有可移植性的编程接口。

各个厂商可以遵循这些标准接口实现自己的MPI软件包,典型的实现包括开放源码的MPICH、LAMMPI以及不开放源码的IntelMPI。

而对于程序员来说,设计好应用程序并行算法,调用这些接口,链接相应平台上的MPI库,就可以实现基于消息传递的并行计算。

也正是由于MPI提供了统一的接口,该标准受到各种并行平台上的广泛支持,这也使得MPI程序具有良好的移植性。

目前MPI支持多种编程语言,包括Fortran77,Fortran90以及C/C++;同时,MPI支持多种操作系统,包括大多数的类UNIX系统以及Windows系统等;MPI还支持多核、对称多处理机、集群等各种硬件平台。

MPI的最大优点是性能,点到点通信函数模型、可操作数据类型都比较丰富,群组通信的函数库更大。

4.多线程编程基础

4.1线程的概念

进程是资源分配的最小单位,线程是运行的最小单位。

进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,进程在运行过程中创建的资源随着进程的终止而被销毁,所使用的系统资源在进程终止时被释放或关闭。

线程是进程内部的一个执行单元。

系统创建好进程后,实际上就启动执行了该进程的主执行线程,主执行线程以函数地址形式,比如说main,将程序的启动点提供给操作系统。

主执行线程终止了,进程也就随之终止。

每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。

用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。

一个进程中的所有线程都在该进程的虚拟地址空间中,共同使用这些虚拟地址空间、全局变量和系统资源,所以线程间的通信非常方便。

线程的实现可以分为两类:

用户级线程(User-LevelThread)和内核级线程(Kernel-LevelThread)。

用户级线程指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库控制用户线程,操作系统内核不知道多线程的存在,内核资源的分配仍然是按照进程进行分配的;各个用户线程只能在进程内进行资源竞争,因此一个线程阻塞将使得整个进程阻塞。

内核级线程:

由操作系统内核创建和撤销。

内核维护进程及线程的上下文信息以及线程切换。

这些线程可以在全系统内进行资源的竞争,一个内核线程由于I/O操作而阻塞,不会影响其他线程的运行。

一般现在比较常见的线程库,如OpenMP和Pthreads线程库都是内核级线程。

Windows线程库既支持内核级线程也支持用户级线程。

多线程的优点:

创建一个线程比创建一个进程的代价要小、线程的切换比进程间的切换代价小、充分利用多处理器、线程间数据共享、通信比进程更高效。

进程间通信的方式:

管道、信号量、消息队列、套接字。

Ø线程有四种状态:

就绪、运行、阻塞、终止。

就绪(ready):

线程等待可用的处理器。

运行(running):

线程正在被执行。

阻塞(blocked):

线程正在等待某个事件的发生(比如I/O的完成,试图加锁一个被上锁的互斥量)。

终止(terminated):

线程从起始函数中返回或者调用pthread_exit。

图4-1线程状态的换图转

4.2线程的同步

由于线程共享同一进程的内存空间,多个线程可能需要同时访问同一个数据。

如果没有正确的保护措施,对共享数据的访问会造成数据的不一致和错误。

常用的线程同步机制:

临界区、锁(其中的互斥量用得比较的多)、信号量、事件。

临界区(CriticalSection):

保证某一时刻只有一个线程访问某一资源的简便方法。

任意时刻只允许一个线程对资源的访问。

如果同一时刻由多个线程试图访问临界区,则其中一个线程进入临界区后,其它线程将会被挂起等待,并一直持续到该线程退出临界区。

当临界区被释放后,其它线程可以抢占,并以此达到原子方式操作共享资源的目的。

互斥量(Mutex):

互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限。

由于互斥对象只有一个,因此就决定了同时只有一个线程拥有对资源的访问权限。

当前占据资源的线程在任务处理完成后交付互斥对象,以便其它线程可以拥有该互斥对象实现对资源的控制访问。

信号量(Semaphores):

信号量对线程同步的方法与前几种不同,信号量允许多个线程同时使用共享资源,这与操作系统中的PV操作相同。

它指出了同时访问共享资源的最大线程数目。

它允许多个线程在同一时刻访问同一资源,但是限制了同一时刻访问资源的最大线程数目。

在用CreateSemaphore()创建信号量时,要同时指出允许访问的最大资源基数和当前可用资源计数。

一般把当前可用资源计数作为最大资源计数,即每增加一个线程对资源的访问相应的资源计数就会减一,只要当前可用资源计数是大于0的,就可以发出信号量信号。

但是当前可用资源减小到小于0时,则说明当前占用资源的线程数目已经达到最大,不允许线程的继续进入,此时信号量也无法发出。

当线程处理完资源后,应该离开的同时通过ReleaseSemaphore()使资源计数加1。

在任何时候当前可用资源数目不可用大于最大资源数目。

事件(Event)

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 医药卫生 > 基础医学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1