OpenMP编程简介一种面向共享内存以及分布式共享内存的多处理器多线程并行编程语言。一种能够被用于显示指导多线程、共享内存并行的应用程序编程接口(API)。OpenMP具有良好的可移植性,支持多种编程语言OpenMP能够支持多种平台,包括大多数的类UNIX系统以及WindowsNT系统(Windows2000,WindowsXP,WindowsVista等)。OpenMP标准诞生于1997年。编程简介OpenMP最初是为共享内存的多处理器系统设计的并行编程方法,这种计算机对程序员来说是多个处理器共享同一个内存设备,其体系结构如图所示:OpenMP多线程编程基础OpenMP的编程模型以线程为基础,通过编译指导语句来显示地指导并行化,为编程人员提供了对并行化的完整控制。OpenMP的执行模型采用Fork-Join的形式:Fork-Join的形式派生线程遇到编译指导语句将派生出另外一组线程MasterThreadParalllRegionNestedParallelRegionOpenMP编程组成OpenMP同时支持C/C++语言和Fortran语言,可以选择任意一种语言以及支持OpenMP的编译器编写OpenMP程序。OpenMP的功能由两种形式提供:编译指导语句与运行时库函数,并通过环境变量的方式灵活控制程序的运行。编译指导语句提供了将一个串行程序渐进的改造为并行程序的能力,而对于不支持OpenMP编译指导语句的编译器,这些编译指导语句又可以被忽略,完全和原来的串行程序兼容。运行时库函数,则只有在必须的情况下才考虑调用。编译指导语句在编译器编译程序的时候,会识别特定的注释,而这些特定的注释就包含着OpenMP程序的一些语义。(#pragmaompparallel).在一个无法识别OpenMP语意的普通编译器中,这些特定的注释会被当作普通的注释而被忽略。在C/C++程序中,OpenMP的所有编译制导语句以#pragmaomp开始,后面跟具体的功能指令。即具有如下的形式:•#pragmaompdirective[clause[[,]clause]…]其中directive部分就包含了具体的编译指导语句,包括parallel,for,parallelfor,section,sections,single,master,critical,flush,ordered和atomic。•将串行的程序逐步地改造成一个并行程序,达到增量更新程序的目的,减少程序编写人员一定的负担。运行时库函数OpenMP运行时函数库原本用以设置和获取执行环境相关的信息,它们当中也包含一系列用以同步的API。支持运行时对并行环境的改变和优化,给编程人员足够的灵活性来控制运行时的程序运行状况。环境变量(OMP_NUM_THREADS)编写OpenMP程序开发工具已经增加了对OpenMP的支持,VisualStudio2005完全支持OpemMP。编写OpenMP程序的必要步骤:•生成项目;•配置项目,支持OpenMP;•编写代码,加速#include“omp.h”;•编写源程序;•配置环境变量,确定线程的数目;•执行程序。循环并行化循环并行化编译制导语句的格式,在C/C++语言中,循环并行化语句的编译指导语句格式如下:#pragmaompparallelfor[clause[clause…]]for(index=first;test_expression;increment_expr){bodyoftheloop;}parallel关键字将紧跟的程序块扩展为若干完全等同的并行区域,每个线程拥有完全相同的并行区域;关键字for则将循环中的工作分配到线程组中,线程组中的每一个线程完成循环中的一部分内容。循环并行化语句的限制并行化语句必须是for循环语句并具有规范的格式,能够推测出循环的次数,有以下约束:•循环语句中的循环变量必须是有符号整型;•循环语句中的比较操作必须是这样的形式:loop_variable,=,或=loop_invariant_integer;•循环语句中的第三个表达式(for循环的循环步长)必须是整数加或整数减,加减的数值必须是一个循环不变量(loopinvariantvalue);•如果比较操作是或=,那么循环变量的值在每次迭代时都必须增加;相反地,如果比较操作是或=,那么循环变量的值在每次迭代时都必须减少;•循环必须是单入口、单出口的,循环内部不允许有能够到达循环之外的跳转语句,也不允许有外部的跳转语句到达循环内部。简单循环并行化两个向量相加,并将计算的结果保存到第三个向量中,向量的维数为n。向量相加即向量的各个分量分别相加。for(inti=0;in;i++)z[i]=x[i]+y[i];存在循环依赖性实例:for(inti=0;in;i++)z[i]=z[i-1]+x[i]+y[i]对于向量加法来说,可以使用循环并行化编译指导语句直接对循环进行并行化:#pragmaompparallelforfor(inti=0;in;i++)z[i]=x[i]+y[i];数据相关的概念如果语句S2与语句S1存在数据相关,那么必然存在以下两种情况之一:•S1在循环的一次迭代中访问存储单元L,而S2在随后的一次迭代中访问同一存储单元,即:循环迭代相关;•S1和S2在同一循环迭代中访问同一存储单元L,但S1的执行在S2之前,即:非循环迭代相关。实例:x[0]=0;y[0]=1;#pragmaompparallelforprivate(k)for(k=1;k100;k++){x[k]=y[k-1]+1;//S1y[k]=x[k-1]+2;//S2}x[0]=0;y[0]=1;x[49]=74;y[49]=74;#pragmaompparallelforprivate(m,k)for(m=0;m2;m++){for(k=m*49+1;km*50+50;k++){x[k]=y[k-1]+1;//S1y[k]=x[k-1]+2;//S2}}循环并行化编译指导语句的子句循环并行化子句可以包含一个或者多个子句来控制循环并行化的实际执行,可以用来控制循环并行化编译。数据的作用域子句用shared来表示一个变量是各个线程之间共享的,而用private来表示一个变量是每一个线程私有的,用threadprivate表示一个线程私有的全局变量。循环嵌套在一个循环体内经常会包含另外一个循环体,循环产生了嵌套。循环并行化编译制导语句可以加在任意一个循环之前,则对应的最近的循环语句被并行化,其它部分保持不变。实际上并行化是作用于嵌套循环中的某一个循环,其它部分由执行到的线程负责执行。#includestdafx.h#includeomp.hint_tmain(intargc,_TCHAR*argv[]){inti;intj;#pragmaompparallelfor//#pragmaompparallelforprivate(j)for(i=0;i4;i++)for(j=6;j10;j++)printf(i=%dj=%d\n,i,j);printf(######################\n);for(i=0;i4;i++)#pragmaompparallelforfor(j=6;j10;j++)printf(i=%dj=%d\n,i,j);return0;}控制数据的共享属性OpenMP程序在同一个共享内存空间上执行,线程通信容易,一个线程写入一个变量,另一个线程可以读取这个变量来完成线程间的通信。控制数据的共享属性全局变量以及程序代码都是全局共享的;而动态分配的堆空间也是共享的。通过threadprivate来明确指出的某一个数据结构属于线程范围的全局变量。OpenMP允许线程保留自己的私有变量不能让其它线程访问到。每一个线程会建立变量的私有拷贝,虽然变量名是相同的,实际上在共享内存空间内部的位置是不同的。控制数据的共享属性数据作用域子句用来确定数据的共享属性,有下面以下几个子句:•shared用来指示一个变量的作用域是共享的;•private用来指示一个变量作用域是私有的;•firstprivate和lastprivate分别对私有的变量进行初始化的操作和最后终结的操作,firstprivate将串行的变量值拷贝到同名的私有变量中,在每一个线程开始执行的时候初始化一次。而lastprivate则将并行执行中的最后一次循环的私有变量值拷贝的同名的串行变量中;•default语句用来改变变量的默认私有属性。在使用作用域子句的时候要遵循如下的一些规则:•作用域子句作用的变量是已经申明的有名变量;•作用域子句在作用到类或者结构的时候,必须作用到类或者结构的整体,而不能只作用于类或者结构的一个部分;•一个编译指导语句能够包含多个数据作用域子句,但是变量只能出现在一个作用域子句中,即变量不能既是共享的,又是私有的;•在语法结构上,作用域子句只能作用在出现在编译指导语句起作用的语句变量部分。另外,可以将作用域子句作用在类的静态变量上。OpenMP对默认情况下,并行区中所有的变量都是共享的,但有三种例外情况:•在parallelfor循环中,循环索引变量是私有的;•那些并行区中的局部变量是私有的;•所有在private,firstprivate,lastprivate或reduction子句中列出的变量都是私有的。私有化是通过为每个线程创建各个变量的独立副本来完成的。规约操作的并行化常见的规约操作——数组求和#pragmaompparallelforprivate(arx,ary,n)reduction(+:a,b)for(i=0;in;i++){a=a+arx[i];b=b+ary[i];}运算符数据类型默认初始值+整数,浮点0*整数,浮点1-整数,浮点0&整数所有位都开启,~0|整数0^整数0&&整数1||整数0使用reduction子句进行多线程程序设计时,要记住以下三个要点:•在第一个线程到达指定了reduction子句的共享区域或循环末尾时,原来的规约变量的值变为不确定,并保持此不确定状态直至规约计算完成;•如果在一个循环中使用到了reduction子句,同时又使用了nowait子句,那么在确保所有线程完成规约计算的栅栏同步操作前,原来的规约变量的值将一直保持不确定的状态;•各个线程的私有副本值被规约的顺序是未指定的。因此,对于同一段程序的一次串行执行和一次并行执行,甚至两次并行执行来说,都无法保证得到完全相同的结果(这主要针对浮点计算而言),也无法保证计算过程中诸如浮点计算异常这样的行为会完全相同。私有变量的初始化和终结操作开始时访问到私有变量在主线程中的同名变量的值,也有可能需要将循环并行化最后一次循环的变量结果返回给主线程中的同名的变量。OpenMP编译指导语句使用firstprivate和lastprivate对这两种需求进行支持,使得循环并行开始执行的时候私有变量通过主线程中的变量初始化,同时循环并行结束的时候,将最后一次循环的相应变量赋值给主线程的变量。程序实例:私有变量的初始化和终结操作实例数据相关性与并行化操作并不是所有的循环都能够使用#pragmaompparallelfor来进行并行化。为了对一个循环进行并行化操作,我们必须要保证数据两次循环之间不存在数据相关性。数据竞争——当两个线程对同一个变量进行操作,并且有一个操作为写操作的时候,就说明这两个线程存在数据竞争,此时,读出的数据不一定就是前一次写操作的数据,而写入的数据也可能并不是程序所需要的。for(inti=0;i99;i++)a[i]=a[i]+a[i+1];具有循环之间数据相关性的串行程序for(intj=1;jN;j++)for(inti=0;iN;i++)a[i,j]=a[i,j]+a[i,j-1];