openmp的库函数和编译错误缺少语句结束指导语句的区别

1734人阅读
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:8331次
排名:千里之外Project_3DFDTD(10)
OpenMP的基本介绍
经过使用,觉得利用其来编写多线程应用程序确实蛮简单的,但是我只用到了,比较简单的部分。但是我觉得最应该注意的是你的代码需不需要进行并行化,并要测试并行化后代码的执行效率是否变高了!并且也要考虑CPU的核心数。
1.简介:OpenMP(OpenMulti-Processing)是一套支持跨平台方式的多线程并发的编程,使用,和语言,可以在大多数的处理器体系和操作系统中运行,包括,,
和。包括一套编译器指令、库和一些能够影响运行行为的环境变量。
OpenMP采用可移植的、可扩展的模型,为程序员提供了一个简单而灵活的开发平台,从标准桌面电脑到超级计算机的并行应用程序接口。
OpenMP是一个跨平台的多线程实现,主线程(顺序的执行指令)生成一系列的子线程,并将任务划分给这些子线程进行执行。这些子线程并行的运行,由将线程分配给不同的处理器。
2.历史:OpenMPArchitecture Review Board (ARB)于1997年10月发布了OpenMP
for Fortran 1.0。次年的10月,发布了C/C++的标准。2000年,发布了Fortran语言的2.0版本,并于2002年发布了C/C++语言的2.0版本。2005年,包含Fortran和C/C++的2.5版本发布了。
在2008年5月发布了3.0版。3.0中的新功能包括任务(tasks)和任务结构(task
construct)的概念。这些新功能总结在OpenMP3.0规范的附录F中。 OpenMP规范的3.1版于2011年7月9日发布。
4.0版本在2013年7月发布,它增加或改进以下功能:支持加速器,原子性,错误处理,线程关联,任务扩展,减少用户定义的SIMD支持和Fortran
2003的支持。
3.优点:进入多核时代后,必须使用多线程编写程序才能让各个CPU核得到利用。在单核时代,通常使用操作系统提供的API来创建线程,然而,在多核系统中,情况发生了很大的变化,如果仍然使用操作系API来创建线程会遇到一些问题。具体来说,有以下三个问题:
1)CPU核数扩展性问题
多核编程需要考虑程序性能随CPU核数的扩展性,即硬件升级到更多核后,能够不修改程序就让程序性能增长,这要求程序中创建的线程数量需要随CPU核数变化,不能创建固定数量的线程,否则在CPU核数超过线程数量上的机器上运行,将无法完全利用机器性能。虽然通过一定方法可以使用操作系统API创建可变化数量的线程,但是比较麻烦,不如OpenMP方便。
2)方便性问题
在多核编程时,要求计算均摊到各个CPU核上去,所有的程序都需要并行化执行,对计算的负载均衡有很高要求。这就要求在同一个函数内或同一个循环中,可能也需要将计算分摊到各个CPU核上,需要创建多个线程。操作系统API创建线程时,需要线程入口函数,很难满足这个需求,除非将一个函数内的代码手工拆成多个线程入口函数,这将大大增加程序员的工作量。使用OpenMP创建线程则不需要入口函数,非常方便,可以将同一函数内的代码分解成多个线程执行,也可以将一个for循环分解成多个线程执行。
3)可移植性问题
目前各个主流操作系统的线程API互不兼容,缺乏事实上的统一规范,要满足可移植性得自己写一些代码,将各种不同操作系统的api封装成一套统一的接口。OpenMP是标准规范,所有支持它的编译器都是执行同一套标准,不存在可移植性问题。
4.编程模型
OpenMP的编程模型以线程为基础,通过编译指导语句来显示的指导并行化。其执行模型采用Fork-Join的形式。其三个组成部分为:1.编译指导语句;2.运行时库函数;3.环境变量。
1.&&&&&编译指导语句:#pragma omp &directive(具体的编译指导语句)&
[clause[ [,]]…](编译指导语句的参数)
四个最常用的OpenMP库函数:
intomp_get_num_threads(void):放回当前使用的线程数;
int omp_set_num_threads(int NumThreads);(此函数可以覆盖环境变量OMP_NUM_THREADS的值);
int omp_get_thread_num(void);返回当前线程号
int omp_get_num_procs(void);返回可用的处理器个数;
循环并行化编译指导语句的子句
& 1.数据作用域子句:其用来控制变量是否在个线程之间共享或是某一个线程所私有;其中shared为共享,private表示某个线程私有。默认为
&&2.控制线程调度的语句:schedule子句
& 3.动态控制是否并行话子句:if语句
& 4.进行同步的子句:ordered子句
& 5.控制变量在串行部分与并行部分传递的子句:copyin子句
循环并行化采用工作分配的执行方式,将循环所需的工作量按照一定的方式分配到各个执行线程中,所有线程执行工作的总和是原先串行所完成的工作量;
4.&&&&&并行区域parallel语句的作用:当程序遇到此编译语句时,就会生成响应数目的线程,且组成一个线程组,并将代码重复地在各个线程内部执行。parallel末尾有一个隐含的屏障,所有线程完成所需的重复任务后,将这个同步屏障汇合。此线程组的主线程继续执行,而相应的子线程则停止执行;
工作分区编码(sections)(在使用工作分区编码的时候,各个线程自动从各个分区中获得任务执行,并且在执行完一个分区的时候,如果分区里还有未完成的工作,则继续取得任务完成。)
#pragma omp parallel sections
#pragra omp section
#pragra omp section
OpenMP支持两种不同类型的同步机制:
互斥锁机制:用来保护一块共享的存储空间,使得每一次访问此共享存储区的线程最多只有一个,从而保证了数据的完整性。
事件通知机制:保证了多个线程之间的执行顺序。控制规定线程执行顺序所需要的同步屏障,弟定序区段,主线程的执行等。
5多线程应用程序的性能分析:
影响性能的因素:
OpenMP本身的开销:
1.并不是所有的并行化都可以带来好的高效率;
2.使用OpenMP进行并行化之后OpenMP本身引入的开销。因为OpenMP获得应用程序多线程并行化的能力需要程序库的支持,库中代码的运行会带来一定的开销;
3.只有并行化代码段负担足够大,而引入OpenMP本身的开销足够小,此时引入并行化操作才能加速程序的执行。
负载均衡:
如果线程之间的负载不均衡,就有可能造成某些线程在执行过程中无事可干,经常处于空闲状态;而另一些线程则负担沉重,需要很长时间才能够完成任务。
线程同步带来的开销:
1.&&&&&多个线程必然带来一定的同步开销,有的同步开销是不可避免的,有的同步则是不必要的;
2.&&&&&考虑同步的必要性,消除不必要的同步或调整同步的顺序,就有可能带来性能上的提升。
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:69776次
积分:2345
积分:2345
排名:第11236名
原创:171篇
转载:19篇
(2)(2)(2)(1)(1)(1)(15)(13)(21)(22)(44)(23)(14)(22)(7)(1)11729人阅读
OpenMP(1)
实用记事(17)
OpenMP是一种API,用于编写可移植的多线程应用程序,无需程序员进行复杂的线程创建、同步、负载平衡和销毁工作。
使用OpenMP的好处:
& &1)CPU核数扩展性问题
& &2)方便性问题
& &3)可移植性问题
OpenMP指令和库函数介绍:
& &在C/C++中,OpenMP指令使用的格式为:#pragma omp 指令 [子句[子句]…]
& &用法详见OpenMP编程指南。
#pragma omp parallel for
& & & &for(i=0;i&i++)
& & & & & &//没有循环迭代相关的语句,如把图像数组中的RGB值转为灰度值。
对可以以多线程执行的循环的约束:
1)循环变量必须是有符号整型,如果是无符号整型,就无法使用
2)比较操作必须是&,&,&=,&=
3)循环步长必须是整数加或整数减操作,加减的操作必须是一个不变量
4)如果是&,&=,循环变量的值每次迭代时必须增加,否则减小
5)循环内部不允许有能够到达循环之外的跳转语句,也不允许有外部的跳转语句到达循环内部。exit语句例外,goto 和break的跳转范围必须在循环内部,异常处理也必须在循环内部处理
数据相关(以下假设为语句S2与语句S1存在数据相关):
相关的种类(相关不等于循环迭代相关):
1)流相关:S1先写某一存储单元,而后S2又读该单元
2)输出相关:两个语句写同一存储单元
3)反相关:一个语句先读一单元,然后另一语句写该单元
相关产生的方式:
1)S1在循环的一次迭代中访问存储单元L,S2在随后的一次迭代中访问L(是循环迭代相关)
2)S1和S2在同一循环迭代中访问同一存储单元L,但S1的执行在S2之前。(非循环迭代相关)
数据竞争:
& & & 数据竞争可能是由于输出相关引起的,编译器不会进行数据竞争的检测,Intel线程检测器可以检测数据竞争。
用类似于互斥量的机制进行私有化和同步,可以消除数据竞争。
#pragma omp parallel for private(x)
& & & &for(i=0;i&80;i++)
& & & & &x=sin(i);
& & & & &if(x&0.6)x=0.6;
& & & & &printf(&sin(%d)=%f\n&,i,x);&
管理共享数据和私有数据:
private:每个线程都拥有该变量的一个单独的副本,可以私有的访问
& & & & &1)private:说明列表中的每个变量对于每个线程都应该有一个私有副本。这个私有副本用变量的默认值进行初始化
& & & & &2)firstprivate:见13数据的Copy-in 和Copy-out
& & & & &3)lastprivate:见13数据的Copy-in 和Copy-out
& & & & &4)reduction:
& & & & &5)threadprivate:指定由每个线程私有的全局变量
有三种方法声明存储单元为私有:
& & & & &1)使用private,firstprivate,lastprivate,reduction子句
& & & & &2)使用threadprivate
& & & & &3)在循环内声明变量,并且不使用static关键字
shared:所有线程都能够访问该单元,并行区域内使用共享变量时,如果存在写操作,必须对共享变量加以保护
default:并行区中所有变量都是共享的,除下列三种情况下:
& & & & & 1)在parallel for循环中,循环索引时私有的。
& & & & & 2)并行区中的局部变量是私有的
& & & & & 3)所有在private,firstprivate,lastprivate,reduction子句中列出的变量是私有的
循环调度与分块
& & &为了提供一种简单的方法以便能够在多个处理器之间调节工作负载,OpenMP给出了四种调度方案:
static,dynamic,runtime,guided.
& & &默认情况下,OpenMP采用静态平均调度策略,但是可以通过调用schedule(kind[,chunksize])子句提供循环调度信息
如:#pragma omp for schedule (kind[,chunk-size]) & //chunk-size为块大小
guided根据环境变量里的设置来进行对前三种的调度
在windows环境中,可以在”系统属性|高级|环境变量”对话框中进行设置环境变量。
有效地使用归约:
for(k=0;k&100;k++)
& & sum=sum+func(k);
& & &为了完成这种形式的循环计算,其中的操作必须满足算术结合律和交换律,同时sum是共享的,这样循环内部都可以加给这个变量,同时又必须是私有的,以避免在相加时的数据竞争。
reduction子句可以用来有效地合并一个循环中某些关于一个或多个变量的满足结合律的算术归约操作。reduction子句主要用来对一个或多个参数条目指定一个操作符,每个线程将创建参数条目的一个私有拷贝,在区域的结束处,将用私有拷贝的值通过指定的运行符运算,原始的参数条目被运算结果的值更新。
#pragma omp parallel for reduction(+:sum)
for(k=0;k&100;k++)
& & sum=sum+func(k);
降低线程开销:当编译器生成的线程被执行时,循环的迭代将被分配给该线程,在并行区的最后,所有的线程都被挂起,等待共同进入下一个并行区、循环或结构化块。
& & & & & & & 如果并行区域、循环或结构化块是相邻的,那么挂起和恢复线程的开销就是没必要的。
举例如下:
& & & & & & & & #pragma omp parallel //并行区内
& & & & & & & & {
& & & & & & & & & &#pragma omp for // 任务分配for循环
& & & & & & & & & & & & & for(k=0;k&m;k++){
& & & & & & & & & & & & & & & &fun1(k);
& & & & & & & & & & & & & &}
& & & & & & & & & &#pragma omp for
& & & & & & & & & & & & & for(k=0;k&m;k++){
& & & & & & & & & & & & & & & &fun2(k);
& & & & & & & & & & & & & &}
& & & & & & & & }
10.任务分配区:
& & &现实中应用程序的所有性能敏感的部分不是都在一个并行区域内执行,所以OpenMP用任务分配区这种结构来处理非循环代码。
任务分配区可以指导OpenMP编译器和运行时库将应用程序中标示出的结构化块分配到用于执行并行区域的一组线程上。
举例如下:
& & & & & & & #pragma omp parallel //并行区内
& & & & & & & & {
& & & & & & & & & &#pragma omp for // 任务分配for循环
& & & & & & & & & & & & & for(k=0;k&m;k++){
& & & & & & & & & & & & & & & &fun1(k);
& & & & & & & & & & & & & &}
& & & & & & & & & &#pragma omp sections private(y,z)
& & & & & & & & & & &{
& & & & & & & & & & & & & &#pragme omp section//任务分配section
& & & & & & & & & & & & & & & &{y=sectionA(x);}
& & & & & & & & & & & & & &#pragme omp section
& & & & & & & & & & & & & & & &{z=sectionB(x);}
& & & & & & & & & & &} & & & & & & & & &&
& & & & & & & & }
使用Barrier和Nowait:
& & & 栅障(Barrier)是OpenMP用于线程同步的一种方法。线程遇到栅障是必须等待,直到并行区中的所有线程都到达同一点。
注意:在任务分配for循环和任务分配section结构中,我们已经隐含了栅障,在parallel,for,sections,single结构的最后,也会有一个隐式的栅障。
隐式的栅障会使线程等到所有的线程继续完成当前的循环、结构化块或并行区,再继续执行后面的工作。可以使用nowait去掉这个隐式的栅障
去掉隐式栅障,例如:
& & & & & & & & #pragma omp parallel //并行区内
& & & & & & & & {
& & & & & & & & & &#pragma omp for nowait // 任务分配for循环
& & & & & & & & & & & & & for(k=0;k&m;k++){
& & & & & & & & & & & & & & & &fun1(k);
& & & & & & & & & & & & & &}
& & & & & & & & & &#pragma omp sections private(y,z)
& & & & & & & & & & &{
& & & & & & & & & & & & & &#pragme omp section//任务分配section
& & & & & & & & & & & & & & & &{y=sectionA(x);}
& & & & & & & & & & & & & &#pragme omp section
& & & & & & & & & & & & & & & &{z=sectionB(x);}
& & & & & & & & & & &} & & & & & & & & &&
& & & & & & & & }
& & &因为第一个 任务分配for循环和第二个任务分配section代码块之间不存在数据相关。
加上显示栅障,例如:
& & & & & & & & & & & & & & & #pragma omp parallel shared(x,y,z) num_threads(2)//使用的线程数为2
& & & & & & & & & & & & & & & &{
& & & & & & & & & & & & & & & & & &int tid=omp_get_thread_num();
& & & & & & & & & & & & & & & & & &if(tid==0)
& & & & & & & & & & & & & & & & & & & &y=fun1();//第一个线程得到y
& & & & & & & & & & & & & & & & & &else&
& & & & & & & & & & & & & & & & & & & & z=fun2();//第二个线程得到z
& & & & & & & & & & & & & & & & & &#pragma omp barrier //显示加上栅障,保证y和z在使用前已有值
& & & & & & & & & & & & & & & & & &#pragma omp for
& & & & & & & & & & & & & & & & & & & & & &for(k=0;k&100;k++)
& & & & & & & & & & & & & & & & & & & & & & & & & &x[k]=y+z;
& & & & & & & & & & & & & & & &}
单线程和多线程交错执行:
& & & 当开发人员为了减少开销而把并行区设置的很大时,有些代码很可能只执行一次,并且由一个线程执行,这样单线程和多线程需要交错执行
举例如下:
& & & & & & & &#pragma omp parallel //并行区
& & & & & & & {
& & & & & & & & & & int tid=omp_get_thread_num();//每个线程都调用这个函数,得到线程号
& & & & & & & & & & &//这个循环被划分到多个线程上进行
& & & & & & & & & & & #pragma omp for nowait
& & & & & & & & & & & for(k=0;k&100;k++)
& & & & & & & & & & & & & & x[k]=fun1(tid);//这个循环的结束处不存在使所有线程进行同步的隐式栅障
& & & & & & & & & & #pragma omp master
& & & & & & & & & & & y=fn_input_only(); //只有主线程会调用这个函数
& & & & & & & & & & #pragma omp barrier & //添加一个显示的栅障对所有的线程同步,从而确保x[0-99]和y处于就绪状态
& & & & & & & & & & &//这个循环也被划分到多个线程上进行
& & & & & & & & & & #pragma omp for nowait
& & & & & & & & & & & for(k=0;k&100;k++)
& & & & & & & & & & & & &x[k]=y+fn2(x[k]); //这个线程没有栅障,所以不会相互等待
& & & & & & & & & & &//一旦某个线程执行完上面的代码,不需要等待就可以马上执行下面的代码
& & & & & & & & & & &#pragma omp single //注意:single后面意味着有隐式barrier
& & & & & & & & & & &fn_single_print(y);
& & & & & & & & & & & //所有的线程在执行下面的函数前会进行同步
& & & & & & & & & & &#pragma omp master
& & & & & & & & & & &fn_print_array(x);//只有主线程会调用这个函数
& & & & & & & }&
数据的Copy-in 和Copy-out:
& & & 在并行化一个程序的时候,一般都必须考虑如何将私有变量的初值复制进来(Copy-in ),以初始化线程组中各个线程的私有副本。
在并行区的最后,还要将最后一次迭代/结构化块中计算出的私有变量复制出来(Copy-out),复制到主线程中的原始变量中。
firstprivate:使用变量在主线程的值对其在每个线程的对应私有变量进行初始化。一般来说,临时私有变量的初值是未定义的。
lastprivate:可以将最后一次迭代/结构化块中计算出来的私有变量复制出来,复制到主线程对应的变量中,一个变量可以同时用firstprivate和lastprivate来声明。
copyin:将主线程的threadprivate变量的值复制到执行并行区的每个线程的threadprivate变量中。
copyprivate:使用一个私有变量将某一个值从一个成员线程广播到执行并行区的其他线程。该子句可以关联single结构(用于single指令中的指定变量为多个线程的共享变量),在所有的线程都离开该结构中的同步点之前,广播操作就已经完成。
保护共享变量的更新操作:
& & &OpenMP支持critical和atomic编译指导,可以用于保护共享变量的更新,避免数据竞争。包含在某个临界段且由atomic编译指导所标记的代码块可能只由一个线程执行。
例如:#pragma omp critical
& & & & & & & if(max&new_value) max=new_
& & & & &}
OpenMP库函数(#include &omp.h&):
int omp_get_num_threads(void); //获取当前使用的线程个数
int omp_set_num_threads(int NumThreads);//设置要使用的线程个数
int omp_get_thread_num(void);//返回当前线程号
int omp_get_num_procs(void);//返回可用的处理核个数
& &编译OpenMP要需要一个支持OpenMP的编译器和线程安全的运行时库。vs2005的配置属性C/C++语言里提供对OpenMP的支持。
& &编译时假如出现&没有找到vcompd.dll,因此这个应用程序未能启动。重新安装应用程序可能会修复此问题&,
可能的原因是该项目有可能是从VC移植过来的,如果由VS创建,一般不会出现该问题,因为VS会解决在清单文件的调用dll问题。
解决方法如下:
StdAfx.h中加入 #pragma comment(linker, &\&/manifestdependency:type='Win32' name='Microsoft.VC80.DebugOpenMP' version='8.0.50608.0' processorArchitecture='X86' publicKeyToken='1fc8b3b9a1e18e3b' language='*'\&&)&
或者在Linker -& Manifest File -& Additional Manifest Dependencies -& 中加入:
&type='Win32' name='Microsoft.VC80.DebugOpenMP' version='8.0.50608.0' processorArchitecture='X86' publicKeyToken='1fc8b3b9a1e18e3b' language='*'&
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:219232次
积分:2955
积分:2955
排名:第8217名
原创:60篇
转载:68篇
评论:48条
(1)(1)(1)(4)(1)(1)(1)(1)(2)(1)(1)(1)(2)(9)(2)(1)(1)(1)(6)(3)(1)(1)(3)(1)(2)(2)(1)(4)(1)(1)(1)(2)(1)(5)(5)(3)(9)(4)(5)(3)(15)(13)(4)trackbacks-0
Visual C++& 2008对OpenMP的支持
&&&&&& VC++2008根据项目属性配置的指示进行 /openmp编译器切换,当配置了OpenMP支持后,编译器会提供_OPENMP定义,可以使用#ifdef _OPENMP来决定程序使用或者不使用OpenMP。
配置VC++2008项目,在project的property页面上的C/C++ -& Language 选项卡中将OpenMP Support这项设置为Yes,在需要使用OpenMP函数的cpp文件中引用#include &omp.h&,这样设置就完成了。
OpenMP的环境变量:
OMP_SCHEDULE
控制for循环任务分配结构的调度
OMP_SCHEDULE="guided,2"
OMP_NUM_THREADS
设置默认线程的个数
OMP_SCHEDULE=4
OpenMP的库函数
int omp_get_num_threads(void)
返回当前使用的线程个数,如果在并行区域外则返回1
int omp_set_num_threads(int i)
设置要使用的线程个数,它可以覆盖OMP_NUM_THREADS
int omp_get_thread_num(void)
返回当前线程号,0代表主线程
int omp_get_num_procs(void)
返回可用的处理核(处理器)个数,对于支持超线程技术的处理器被算作两个处理核
OpenMP的调度方案
将所有循环迭代划分成相等大小的块
使用一个内部队列,当某线程可用时,为其分配由块大小所制定的一定数量的循环迭代
与dynamic策略类似,但是块大小开始较大,后来逐步减小。可选参数chunk指定块大小的最小值,默认为1
运行时由OMP_SCHEDULE决定使用上面三种的哪种策略
OpenMP的编译
平台和编译器
windows平台 intel C++编译器
icl /Qopenmp
linux平台 intel C++编译器
icl -openmp
gcc -fopenmp
_OPENMP宏可以用来判断OpenMP是否被支持,通过它可以写出任何C语言编译器(即使不支持OpenMP)都可以编译的代码。代码如下所示:
#ifdef _OPENMP
#include &omp.h&
#define omp_get_thread_num() 0
阅读(...) 评论()3309人阅读
OpenMP(2)
内容摘自《OpenMP编译原理及实现技术》第2章
代码测试环境:Windows7 64bit, VS2010, 4核机。
可以说OpenMP制导指令将C语言扩展为一个并行语言,但OpenMP本身不是一种独立的并行语言,而是为多处理器上编写并行程序而设计的、指导共享内存、多线程并行的编译制导指令和应用程序编程接口(API),可在C/C++和Fortran中应用,并在串行代码中以编译器可识别的注释形式出现。OpenMP标准是由一些具有国际影响力的软件和硬件厂商共同定义和提出。
1.&OpenMP基本概念
1.1 OpenMP执行模式
&&&&&&&& OpenMP的执行模型采用fork-join的形式,其中fork创建线程或者唤醒已有线程;join即多线程的会合。fork-join执行模型在刚开始执行的时候,只有一个称为“主线程”的运行线程存在。主线程在运行过程中,当遇到需要进行并行计算的时候,派生出线程来执行并行任务。在并行执行的时候,主线程和派生线程共同工作。在并行代码执行结束后,派生线程退出或者阻塞,不再工作,控制流程回到单独的主线程中。OpenMP线程:在OpenMP程序中用于完成计算任务的一个执行流的执行实体,可以是操作系统的线程也可以是操作系统上的进程。
1.2 OpenMP编程要素
OpenMP编程模型以线程为基础,通过编译制导指令来显示地指导并行化,OpenMP为编程人员提供了三种编程要素来实现对并行化的完善控制。它们是编译制导、API函数集和环境变量。
1.2.1&编译制导:在C/C++程序中,OpenMP的所有编译制导指令是以#pragma omp开始,后面跟具体的功能指令(或命令)。其中指令或命令是可以单独出现的,而子句则必须出现在制导指令之后。制导指令和子句按照功能可以大体上分成四类:(1)、并行域控制类;(2)、任务分担类;(3)、同步控制类;(4)、数据环境类。并行域控制类指令用于指示编译器产生多个线程以并发执行任务,任务分担类指令指示编译器如何给各个并发线程分发任务,同步控制类指令指示编译器协调并发线程之间的时间约束关系,数据环境类指令处理并行域内外的变量共享或私有属性以及边界上的数据传送操作等。
编译制导指令:版本为2.5的OpenMP规范中的指令:(1)、parallel:用在一个结构块之前,表示这段代码将被多个线程并行执行;(2)、for:用于for循环语句之前,表示将循环计算任务分配到多个线程中并行执行,以实现任务分担,必须由编程人员自己保证每次循环之间无数据相关性;(3)、parallel for:parallel和for指令的结合,也是用在for循环语句之前,表示for循环体的代码将被多个线程并行执行,它同时具有并行域的产生和任务分担两个功能;(4)、sections:用在可被并行执行的代码段之前,用于实现多个结构块语句的任务分担,可并行执行的代码段各自用section指令标出;(5)、parallel
sections:parallel和sections两个语句的结合,类似于parallel for;(6)、single:用在并行域内,表示一段只被单个线程执行的代码;(7)、critical:用在一段代码临界区之前,保证每次只有一个OpenMP线程进入;(8)、flush:保证各个OpenMP线程的数据影像的一致性;(9)、barrier:用于并行域内代码的线程同步,线程执行到barrier时要停下等待,直到所有线程都执行到barrier时才能继续往下执行;(10)、atomic:用于指定一个数据操作需要原子性地完成;(11)、master:用于指定一段代码由主线程执行;(12)、threadprivate:用于指定一个或多个变量是线程专用。
&&&&&&&& OpenMP的子句:(1)、private:指定一个或多个变量在每个线程中都有它自己的私有副本;(2)、firstprivate:指定一个或多个变量在每个线程都有它自己的私有副本,并且私有变量要在进入并行域或认为分担域时,继承主线程中的同名变量的值作为初值;(3)、lastprivate:是用来指定将线程中的一个或多个私有变量的值在并行处理结束后复制到主线程中的同名变量中,负责拷贝的线程是for或sections任务分担中的最后一个线程;(4)、reduction:用来指定一个或多个变量是私有的,并且在并行处理结束后这些变量要执行指定的归约运算,并将结果返回给主线程同名变量;(5)、nowait:指出并发线程可以忽略其他制导指令暗含的路障同步;(6)、num_threads:指定并行域内的线程的数目;(7)、schedule:指定for任务分担中的任务分配调度类型;(8)、shared:指定一个或多个变量为多个线程间的共享变量;(9)、copyprivate:配合single指令,将指定线程的专有变量广播到并行域内其他线程的同名变量中;(10)、copyin:用来指定一个threadprivate类型的变量需要用主线程同名变量进行初始化;(11)、default:用来指定并行域内的变量的使用方式,缺省是shared。
1.2.2 API函数集:用于控制并发线程的某些行为。
(1)、omp_in_parallel:判断当前是否在并行域中;(2)、omp_get_thread_num:返回线程号;(3)、omp_set_num_threads:设置后续并行域中的线程个数;(4)、omp_get_num_threads:返回当前并行区域中的线程数;(5)、omp_get_max_threads:获取并行域可用的最大线程数目;(6)、omp_get_num_procs:返回系统中处理器个数;(7)、omp_get_dynamic:判断是否支持动态改变线程数目;(8)、omp_set_dynamic:启用或关闭线程数目的动态改变;(9)、omp_get_nested:判断系统是否支持并行嵌套;(10)、omp_set_nested:启用或关闭并行嵌套;(11)、omp_init(_nest)_lock:初始化一个(嵌套)锁;(12)、omp_destroy(_nest)_lock:销毁一个(嵌套)锁;(13)、omp_set(_nest)_lock:(嵌套)加锁操作;(14)、omp_unset(_nest)_lock:(嵌套)解锁操作;(15)、omp_test(_nest)_lock:非阻塞的(嵌套)加锁;(16)、omp_get_wtime:获取wall
time时间;(17)、omp_set_wtime:设置wall time时间。
1.2.3 环境变量:可以在一定程度上控制OpenMP程序的行为。
(1)、OMP_SCHEDULE:用于for循环并行化后的调度,它的值就是循环调度的类型;(2)、OMP_NUM_THREADS:用于设置并行域中的线程数;(3)、OMP_DYNAMIC:通过设定变量值,来确定是否允许动态设定并行域内的线程数;(4)、OMP_NESTED:指出是否可以并行嵌套。
1.2.4 ICV:OpenMP规范中定义了一些内部控制变量ICV(Internal Control Variable),用于表示系统的属性、能力和状态等,可以通过OpenMP API函数访问也可以通过环境变量进行修改。但是变量的具体名字和实现方式可以由各个编译器自行决定。
2. OpenMP编程
2.1 并行域管理:在OpenMP的相邻的fork、join操作之间称之为一个并行域,并行域可以嵌套。
2.1.1 parallel的使用方法:
#pragma ompparallel[for | sections][子句[子句]…]
{ … 代码 …}
parallel语句后面要用一个大括号对将要并行执行的代码括起来。为了指定使用多少个线程来执行,可以通过设置环境变量OMP_NUM_THREADS或者调用omp_set_num_threads()函数,也可以使用num_threads子句,前者只能在程序刚开始运行时起作用,而API函数和子句可以在程序中并行域产生之前起作用。
#pragma omp parallel //parallel语句后面要用一个大括号对将要并行执行的代码括起来
{//并行域的开始(对应fork)
printf(&hello, world\n&);
}//并行域的结束(对应join)hello, world
hello, world
hello, world
hello, world
#pragma omp parallel num_threads(8)
printf(&hello, world! Threadid=%d\n&, omp_get_thread_num());
}hello, world! Threadid=0
hello, world! Threadid=4
hello, world! Threadid=2
hello, world! Threadid=3
hello, world! Threadid=6
hello, world! Threadid=7
hello, world! Threadid=1
hello, world! Threadid=5
从Threadid的不同可以看出创建了8个线程来执行以上代码。所以parallel指令是用来产生或唤醒多个线程创建并行域的,并且可以用num_threads子句控制线程数目。parallel域中的每行代码都被多个线程重复执行。和传统的创建线程函数比起来,其过程非常简单直观。parallel的并行域内部代码中,若再出现parallel制导指令则出现并行域嵌套问题,如果设置了OMP_NESTED环境变量,那么在条件许可时内部并行域也会由多个线程执行,反之没有设置相应变量,那么内部并行域的代码将只有一个线程来执行。还有一个环境变量OMP_DYNAMIC也影响并行域的行为,如果没有设置该环境变量将不允许动态调整并行域内的线程数目,omp_set_dynamic()也是用于同样的目的。
2.2 任务分担:当使用parellel制导指令产生出并行域之后,如果仅仅是多个线程执行完全相同的任务,那么只是徒增计算工作量而不能达到加速计算的目的,甚至可能相互干扰得到错误结果。因此在产生并行域之后,紧接着的问题就是如何将计算任务在这些线程之间分配,并加快计算结果的产生速度及其保证正确性。OpenMP可以完成的任务分担的指令只有for、sections和single,严格意义上来说只有for和sections是任务分担指令,而single只是协助任务分担的指令。任务分担域和并行域的定义一样,既是指代码区间也是指执行时间区间。
2.2.1 for制导指令:for指令指定紧随它的循环语句必须由线程组并行执行,用来将一个for循环任务分配到多个线程,此时各个线程各自分担其中一部分工作。for指令一般可以和parallel指令合起来形成parallel for指令使用,也可以单独用在parallel指令的并行域中。
#pragma omp for
for (j=0; j&4; ++j)
printf(&j=%d, Threadid=%d\n&, j, omp_get_thread_num());
}j=0, Threadid=0
j=1, Threadid=0
j=2, Threadid=0
j=3, Threadid=0
从结果可以看出,4次循环都在一个Threadid为0的线程里执行,并没有实现并发执行也不会加快计算速度。可见for指令要和parallel指令结合起来使用才有效果,即for出现在并行域中才能有多个线程来分担任务。
#pragma omp parallel for
for (j=0; j&4; ++j)
printf(&j=%d, Threadid=%d\n&, j, omp_get_thread_num());
j=0, Threadid=0
j=1, Threadid=1
j=3, Threadid=3
j=2, Threadid=2
#pragma omp parallel
#pragma omp for
for (j=0; j&100; ++j)
//do someting
#pragma omp for
for (j=0; j&100; ++j)
//do someting
//do someting
}此时只有一个并行域,在该并行域内的多个线程首先完成第一个for语句的任务分担,然后在此进行一次同步(for制导指令本身隐含有结束处的路障同步),然后再进行第二个for语句的任务分担,直到退出并行域只剩下一个主线程为止。
2.2.2 for调度:当循环中每次迭代的计算量不相等时,如果简单地给各个线程分配相同次数的迭代的话,会使得各个线程计算负载不均衡,这会使得有些线程先执行完,有些后执行完,造成某些CPU核空闲,影响程序性能。在OpenMP的for任务分担中,任务的划分称为调度,各个线程如何划分任务是可以调整的,因此有静态划分、动态划分等,所以调度也分成多个类型。for任务调度子句只能用于for制导指令中。在OpenMP中,对for循环任务调度使用schedule子句来实现。schedule子句使用格式为:schedule(type[,
size]). type参数,表示调度类型,有四种调度类型如下:static、dynamic、guided、runtime。size参数为可选,表示以循环迭代次数计算的划分单位,每个线程所承担的计算任务对应于0个或若干个size次循环,size参数必须是整数。static、dynamic、guided三种调度方式都可以使用size参数,也可以不使用size参数。当type参数类型为runtime时,size参数是非法的。
2.2.2.1 static静态调度:当for或者parallelfor编译制导指令没有带schedule子句时,大部分系统中默认采用size为1的static调度方式。
#pragma omp parallel for schedule(static)
for (i=0; i&10; ++i)
printf(&i=%d, thread_id=%d\n&, i, omp_get_thread_num());
}i=0, thread_id=0
i=1, thread_id=0
i=3, thread_id=1
i=4, thread_id=1
i=5, thread_id=1
i=2, thread_id=0
i=6, thread_id=2
i=7, thread_id=2
i=8, thread_id=3
i=9, thread_id=3
注意:由于多线程执行时序的随机性,每次执行时打印的结果顺序可能存在差别。
#pragma omp parallel for schedule(static, 2)
for (i=0; i&10; ++i)
printf(&i=%d, thread_id=%d\n&, i, omp_get_thread_num());
i=0, thread_id=0
i=1, thread_id=0
i=8, thread_id=0
i=9, thread_id=0
i=6, thread_id=3
i=7, thread_id=3
i=2, thread_id=1
i=3, thread_id=1
i=4, thread_id=2
i=5, thread_id=2
使用size参数时,分配给每个线程的size次连续的迭代计算。
2.2.2.2 dynamic动态调整:是动态地将迭代分配到各个线程,各线程动态的申请任务,因此较快的线程可能申请更多次数,而较慢的线程申请任务次数可能较少,因此动态调整可以在一定程度上避免前面提到的按循环次数划分引起的负载不平衡问题。
#pragma omp parallel for schedule(dynamic)
for (i=0; i&10; ++i)
printf(&i=%d, thread_id=%d\n&, i, omp_get_thread_num());
i=0, thread_id=0
i=4, thread_id=0
i=5, thread_id=0
i=6, thread_id=0
i=7, thread_id=0
i=8, thread_id=0
i=9, thread_id=0
i=1, thread_id=2
i=2, thread_id=1
i=3, thread_id=3
#pragma omp parallel for schedule(dynamic, 2)
for (i=0; i&10; ++i)
printf(&i=%d, thread_id=%d\n&, i, omp_get_thread_num());
i=0, thread_id=0
i=1, thread_id=0
i=2, thread_id=0
i=3, thread_id=0
i=4, thread_id=0
i=5, thread_id=0
i=8, thread_id=0
i=9, thread_id=0
i=6, thread_id=3
i=7, thread_id=3
动态调整时,size小有利于实现更好的负载均衡,但是会引起过多的任务动态申请的开销,反之size大则开销较少,但是不易于实现负载平衡,size的选择需要在这两者之间进行权衡。
2.2.2.3 guided调度:是一种采用指导性的启发式自调度方法。开始时每个线程会分配到较大的迭代块,之后分配到的迭代块会逐渐递减。迭代块的大小会按指数级下降到指定的size大小,如果没有指定size参数,那么迭代块大小最小会降到1.
#pragma omp parallel for schedule(guided, 2)
for (i=0; i&10; ++i)
printf(&i=%d, thread_id=%d\n&, i, omp_get_thread_num());
i=0, thread_id=1
i=1, thread_id=1
i=2, thread_id=1
i=9, thread_id=1
i=3, thread_id=0
i=4, thread_id=0
i=7, thread_id=3
i=8, thread_id=3
i=5, thread_id=2
i=6, thread_id=2
2.2.2.4 runtime调度:它不像static、dynamic、guided三种调度方式那样是真实调度方式。它是在运行时根据环境变量OMP_SCHEDULE来确定调度类型,最终使用的调度类型仍然是static、dynamic、guided中的一种。
2.2.3 sections编译制导指令:是用于非迭代计算的任务分担,它将sections语句里的代码用section制导指令划分成几个不同的段(可以是一条语句,也可以是用{…}括起来的结构块),不同的section段由不同的线程并行执行。
#pragma omp parallel sections
#pragma omp section
printf(&section 1 thread=%d\n&, omp_get_thread_num());
#pragma omp section
printf(&section 2 thread=%d\n&, omp_get_thread_num());
#pragma omp section
printf(&section 3 thread=%d\n&, omp_get_thread_num());
}section 1 thread=0
section 2 thread=2
section 2 thread=1
#pragma omp parallel
#pragma omp sections
#pragma omp section
printf(&section 1 Threadid=%d\n&, omp_get_thread_num());
#pragma omp section
printf(&section 2 Threadid=%d\n&, omp_get_thread_num());
#pragma omp sections//两个sections构造先后串行执行,与for制导指令一样,在sections的结束处有一个隐含的路障同步
#pragma omp section
printf(&section 3 Threadid=%d\n&, omp_get_thread_num());
#pragma omp section
printf(&section 4 Threadid=%d\n&, omp_get_thread_num());
section 1 thread=0
section 2 thread=1
section 3 thread=0
section 4 thread=1
这里有两个sections构造先后串行执行的,即第二个sections构造的代码要等第一个sections构造的代码执行完后才能执行。sections构造里面的各个section部分代码是并行执行的。与for制导指令一样,在sections的结束处有一个隐含的路障同步,没有其他说明的情况下,所有线程都必须到达该点才能往下运行。使用section指令时,需要注意的是这种方式需要保证各个section里的代码执行时间相差不大,否则某个section执行时间比其他section过长就造成了其它线程空闲等待的情况。用for语句来分担任务时工作量由系统自动划分,只要每次循环间没有时间上的差异,那么分摊是比较均匀的,使用section来划分线程是一种手工划分工作量的方式,最终负载均衡的好坏得依赖于程序员。
2.2.4 single制导指令:单线程执行single制导指令指定所包含的代码只由一个线程执行,别的线程跳过这段代码。如果没有nowait从句,所有线程在single制导指令结束处隐式同步点同步。如果single制导指令有nowait从句,则别的线程直接向下执行,不在隐式同步点等待;single制导指令用在一段只被单个线程执行的代码段之前,表示后面的代码段将被单线程执行。#pragma omp single[子句]
#pragma omp parallel
#pragma omp single
printf(&Beginning work1.\n&);
printf(&work on 1 parallelly.%d\n&, omp_get_thread_num());
#pragma omp single
printf(&Finishing work1.\n&);
#pragma omp single nowait
printf(&Beginning work2.\n&);
printf(&work on 2 parallelly.%d\n&, omp_get_thread_num());
}Beginning work1.
work on 1 parallelly.2
Finishing work1.
work on 1 parallelly.1
work on 1 parallelly.0
work on 1 parallelly.3
Beginning work2.
work on 2 parallelly.1
work on 2 parallelly.0
work on 2 parallelly.2
work on 2 parallelly.3
另一种需要使用single制导指令的情况是为了减少并行域创建和撤销的开销,而将多个临界的parallel并行域合并时。经过合并后,原来并行域之间的串行代码也将被并行执行,违反了代码原来的目的,因此这部分代码可以用single指令加以约束只用一个线程来完成。
2.3 同步:在正确产生并行域并用for、sections等语句进行任务分担后,还须考虑的是这些并发线程的同步互斥需求。在OpenMP应用程序中,由于是多线程执行,所以必须有线程互斥机制以保证程序在出现数据竞争的时候能够得出正确的结果,并且能够控制线程执行的先后制约关系,以保证执行结果的正确性。OpenMP支持两种不同类型的线程同步机制,一种是互斥锁的机制,可以用来保护一块共享的存储空间,使任何时候访问这块共享内存空间的线程最多只有一个,从而保证了数据的完整性;另外一种同步机制是事件同步机制,这种机制保证了多个线程之间的执行顺序。互斥的操作针对需要保护的数据而言,在产生了数据竞争的内存区域加入互斥,可以使用包括critical、atomic等制导指令以及API中的互斥函数。而事件机制则控制线程执行顺序,包括barrier同步路障、ordered定序区段、master主线程执行等。
2.3.1 critical临界区:在可能产生内存数据访问竞争的地方,都需要插入相应的临界区制导指令,格式:#pragam omp critical[(name)] &critical语句不允许互相嵌套。
int max_num_x=max_num_y=-1;
#pragma omp parallel for
for (i=0; i&n; ++i)
#pragma omp critical(max_arx);
if (arx[i] &max_num_x)
max_num_x = arx[i];
#pragma omp critical(max_ary)
if (ary[i]&max_num_y)
max_num_y = ary[i];
}在一个并行域内的for任务分担域中,各个线程逐个进入到critical保护的区域内,比较当前元素和最大值的关系并可能进行最大值的更替,从而避免了数据竞争的情况。
2.3.2 atomic原子操作:critical临界区操作能够作用在任意大小的代码块上,而原子操作只能作用在单条赋值语句中。能够使用原子语句的前提条件是相应的语句能够转化成一条机器指令,使得相应的功能能够一次执行完毕而不会被打断。C/C++中可用的原子操作:“+、-、*、/、&、^、&&、&&”。值得注意的是,当对一个数据进行原子操作保护的时候,就不能对数据进行临界区的保护,OpenMP运行时并不能在这两种保护机制之间建立配合机制。用户在针对同一个内存单元使用原子操作的时候,需要在程序所有涉及到该变量并行赋值的部位都加入原子操作的保护。
int counter = 0;
#pragma omp parallel
for (int i=0; i&10000; ++i)
#pragma omp atomic//atomic operation
counter++;
printf(&counter=%d\n&, counter);
counter=4000
由于使用atomic语句,则避免了可能出现的数据访问竞争情况,最后的执行结果都是一致的。而将atomic这一行语句从源程序中删除时,由于有了数据访问的竞争情况,所以最后的执行结果是不确定的。
2.3.3 barrier同步路障:路障(barrier)是OpenMP线程的一种同步方法。线程遇到路障时必须等待,直到并行区域内的所有线程都到达了同一点,才能继续执行下面的代码。在每一个并行域和任务分担域的结束处都会有一个隐含的同步路障,执行此并行域/任务分担域的线程组在执行完毕本区域代码之前,都需要同步并行域的所有线程。也就是说在parallel、for、sections和single构造的最后,会有一个隐式的路障。在有些情况下,隐含的同步路障并不能提供有效的同步措施。这时,需要程序员插入明确的同步路障语句#pragma
omp barrier。此时,在并行区域的执行过程中,所有的执行线程都会在同步路障语句上进行同步。
#pragma omp parallel
Initialization();
omp barrier
Process();
& & & & &只有等所有的线程都完成Initialization()初始化操作以后,才能够进行下一步的处理动作,因此,在此处插入一个明确的同步路障操作以实现线程之间的同步。
2.3.4 nowait:为了避免在循环过程中不必要的同步路障并加快运行速度,可以使用nowait子句除去这个隐式的路障。
#pragma omp parallel num_threads(4)
#pragma omp for nowait
for (int i=0; i&8; ++i)
printf(&+\n&);
#pragma omp for
for (j=0; j&8; ++j)
printf(&-\n&);
此时,线程在完成第一个for循环子任务后,并不需要同步等待,而是直接执行后面的任务,因此出现“-”在“+”前面的情况。nowait子句消除了不必要的同步开销,加快了计算速度,但是也引入了实现上的困难。
2.3.5 master主线程执行:用于指定一段代码由主线程执行。master制导指令和single制导指令类似,区别在于,master制导指令包含的代码段只由主线程执行,而single制导指令包含的代码段可由任一线程执行,并且master制导指令在结束处没有隐式同步,也不能指定nowait从句。
#pragma omp parallel
#pragma omp for
for (i=0; i&5; ++i)
a[i] = i *
#pragma omp master
for (i=0; i&5; ++i)
printf(&a[%d]=%d\n&, i, a[i]);
只有一个线程将逐个元素打印出来。
2.3.6 ordered顺序制导指令:对于循环代码的任务分担中,某些代码的执行需要按规定的顺序执行。典型的情况如下:在一次循环的过程中大部分的工作是可以并行执行的,而特定部分代码的工作需要等到前面的工作全部完成之后才能够执行。这时,可以使用ordered子句使特定的代码按照串行循环的次序来执行。
#pragma omp parallel
#pragma omp for
for (i=0; i&100; ++i)
//一些无数据相关、可并行乱序执行的操作
//do someting
#pragma omp ordered
//一些有数据相关、只能顺序执行的操作
//do someting
虽然在ordered子句之前的工作是并行执行的,但是在遇到ordered子句的时候,只有前面的循环都执行完毕之后,
才能够进行下一步执行。这样一来,有些任务在并行执行,对于部分必须串行执行的部分才启用ordered保护。
2.3.7 互斥锁函数:除了atomic和critical编译制导指令,OpenMP还可以通过库函数支持实现互斥操作,方便用户实现特定的同步需求。编译制导指令的互斥支持只能放置在一段代码之前,作用在这段代码之上。而OpenMP API所提供的互斥函数可放在任意需要的位置。程序员必须自己保证在调用相应锁操作之后释放相应的锁,否则就可能造成多线程程序的死锁。互斥锁函数中只有omp_test_lock函数是带有返回值的,该函数可以看作是omp_set_lock的非阻塞版本。
static omp_lock_
omp_init_lock(&lock);
#pragma omp parallel for
for (i=0; i&5; ++i)
omp_set_lock(&lock);
printf(&%d +\n&, omp_get_thread_num());
printf(&%d -\n&, omp_get_thread_num());
omp_unset_lock(&lock);
omp_destroy_lock(&lock);0 +
示例对for循环中的所有内容进行加锁保护,同时只能有一个线程执行for循环中的内容。
2.4 数据环境控制:通常来说,OpenMP是建立在共享存储结构的计算机之上,使用操作系统提供的线程作为并发执行的基础,所以线程间的全局变量和静态变量是共享的,而局部变量、自动变量是私有的。但是对OpenMP编程而言,缺省变量往往是共享变量,而不管它是不是全局静态变量还是局部自动变量。也就是说OpenMP各个线程的变量是共享还是私有,是依据OpenMP自身的规则和相关的数据子句而定,而不是依据操作系统线程或进程上的变量特性而定。OpenMP的数据处理子句包括private、firstprivate、lastprivate、shared、default、reduction
copyin和copyprivate.它与编译制导指令parallel、for和sections相结合用来控制变量的作用范围。它们控制数据变量,比如,哪些串行部分中的数据变量被传递到程序的并行部分以及如何传送,哪些变量对所有并行部分的线程是可见的,哪些变量对所有并行部分的线程是私有的,等等。
2.4.1 共享与私有化:
2.4.1.1 shared子句:用来声明一个或多个变量是共享变量。需要注意的是,在并行域内使用共享变量时,如果存在写操作,必须对共享变量加以保护,否则不要轻易使用共享变量,尽量将共享变量的访问转化为私有变量的访问。循环迭代变量在循环构造的任务分担域里是私有的。声明在任务分担域内的自动变量都是私有的。
2.4.1.2 default子句:用来允许用户控制并行区域中变量的共享属性。使用shared时,缺省情况下,传入并行区域内的同名变量被当作共享变量来处理,不会产生线程私有副本,除非使用private等子句来指定某些变量为私有的才会产生副本。如果使用none作为参数,除了那些由明确定义的除外,线程中用到的变量都必须显式指定为是共享的还是私有的。
2.4.1.3 private子句:用来将一个或多个变量声明成线程私有的变量,变量声明成私有变量后,指定每个线程都有它自己的变量私有副本,其他线程无法访问私有副本。即使在并行域外有同名的共享变量,共享变量在并行域内不起任何作用,并且并行域内不会操作到外面的共享变量。出现在reduction子句中的变量不能出现在private子句中。
int k = 100;
#pragma omp parallel for private(k)
for (k=0; k&8; ++k)
printf(&k=%d\n&, k);
printf(&last k=%d\n&, k);k=0
last k=100
& & & & &for循环前的变量k和循环区域内的变量k其实是两个不同的变量。用private子句声明的私有变量的初始值在并行域的入口处是未定义的,它并不会继承同名共享变量的值。
2.4.1.4 firstprivate子句:私有变量的初始化和终结操作,OpenMP编译制导指令需要对这种需求给予支持,即使用firstprivate和lastprivate来满足这两种需求。使得并行域或任务分担域开始执行时,私有变量通过主线程中的变量初始化,也可以在并行域或任务分担结束时,将最后一次一个线程上的私有变量赋值给主线程的同名变量。private声明的私有变量不会继承同名变量的值,于是OpenMP提供了firstprivate子句来实现这个功能。firstprivate子句是private子句的超集,即不仅包含了private子句的功能,而且还要对变量做进行初始化。
int i, k=100;
#pragma omp parallel for firstprivate(k)
for (i=0; i&4; ++i)
printf(&k=%d\n&, k);
printf(&last k=%d\n&, k);k=100
last k=100
并行域内的私有变量k继承了外面共享变量k的&#作为初始值,并且在退出并行区域后,共享变量k的值保持为100未变。
2.4.1.5 lastprivate子句:有时要将任务分担域内私有变量的值经过计算后,在退出时,将它的值赋给同名的共享变量(private和firstprivate子句在退出并行域时都没有将私有变量的最后取值赋给对应的共享变量),lastprivate子句就是用来实现在退出并行域时将私有变量的值赋给共享变量。lastprivate子句也是private子句的超集,即不仅包含了private子句的功能,而且还要将变量从for、sections的任务分担域中最后的线程中复制给外部同名变量。由于在并行域内是多个线程并行执行的,最后到底是将哪个线程的最终计算结果赋给了对应的共享变量呢?OpenMP规范中指出,如果是for循环迭代,那么是将最后一次循环迭代中的值赋给对应的共享变量;如果是sections构造,那么是代码中排在最后的section语句中的值赋给对应的共享变量。注意这里说的最后一个section是指程序语法上的最后一个,而不是实际运行时的最后一个运行完的。
int i, k=100;
#pragma omp parallel for firstprivate(k), lastprivate(k)
for (i=0; i&4; ++i)
printf(&k=%d\n&, k);
printf(&last k=%d\n&, k);k=101
last k=103
退出for循环的并行区域后,共享变量k的值变成了103,而不是保持原来的100不变。
2.4.1.6 flush:OpenMP的flush制导指令主要与多个线程之间的共享变量的一致性问题。用法:flush[(list)],该指令将列表中的变量执行flush操作,直到所有变量都已完成相关操作后才返回,保证了后续变量访问的一致性。
2.4.2 线程专有数据:它和私有数据不太相同,threadprivate子句用来指定全局的对象被各个线程各自复制了一个私有的拷贝,即各个线程具有各自私有、线程范围内的全局对象。private变量在退出并行域后则失效,而threadprivate线程专有变量可以在前后多个并行域之间保持连续性。
2.4.2.1 threadprivate子句:用法:#pragma omp threadprivate(list) new-line。用作threadprivate的变量的地址不能是常数。对于C++的类(class)类型变量,用作threadprivate的参数时有些限制,当定义时带有外部初始化则必须具有明确的拷贝构造函数。对于windows系统,threadprivate不能用于动态装载(使用LoadLibrary装载)的DLL中,可以用于静态装载的DLL中。
int counter = 0;
#pragma omp threadprivate(counter)
int increment_counter()
counter++;
return (counter);
}实现一个线程私有的计数器,各个线程使用同一个函数来实现自己的计数。
如果是静态变量也同样可以使用threadprivate声明成线程私有的。
2.4.2.2 copyin子句:用来将主线程中threadprivate变量的值复制到执行并行域的各个线程的threadprivate变量中,便于所有线程访问主线程中的变量值。copyin中的参数必须被声明成threadprivate的,对于class类型的变量,必须带有明确的拷贝赋值操作符。
int counter = 0;
#pragma omp threadprivate(counter)
int increment_counter()
counter++;
return (counter);
int _tmain(int argc, _TCHAR* argv[])
#pragma omp parallel sections copyin(counter)
#pragma omp section
int count1;
for (iterator=0; iterator&100; ++iterator)
count1 = increment_counter();
printf(&count1=%ld\n&, count1);
#pragma omp section
int count2;
for (iterator=0; iterator&200; ++iterator)
count2 = increment_counter();
printf(&count2=%ld\n&, count2);
printf(&counter=%ld\n&, counter); return 0;
}count1=100
count2=200
threadprivate中的计数器函数,如果多个线程使用时,各个线程都需要对全局变量counter的副本进行初始化。
2.4.2.3 copyprivate子句:提供了一种机制,即将一个线程私有变量的值广播到执行同一并行域的其他线程。copyprivate子句可以关联single构造,在single构造的barrier到达之前就完成了广播工作。copyprivate可以对private和threadprivate子句中的变量进行操作,但是当使用single构造时,copyprivate的变量不能用于private和firstprivate子句中。
int counter = 0;
#pragma omp threadprivate(counter)
int increment_counter()
counter++;
return (counter);
int _tmain(int argc, _TCHAR* argv[])
#pragma omp parallel
#pragma omp single copyprivate(counter)
counter = 50;
count = increment_counter();
printf(&Threadid:%ld, count=%ld\n&, omp_get_thread_num(), count);
}Threadid:0, count=51
Threadid:1, count=51
Threadid:3, count=51
Threadid:2, count=51
& & & & &使用copyprivate子句后,single构造内给counter赋的值被广播到了其它线程里,但没有使用copyprivate子句时,只有一个线程获得了single构造内的赋值,其它线程没有获取single构造内的赋值。
2.4.3 归约操作:reduction子句主要用来对一个或多个参数条目指定一个操作符,每个线程将创建参数条目的一个私有拷贝,在并行域或任务分担域的结束处,将用私有拷贝的值通过指定的运行符运算,原始的参数条目被运算结果的值更新。列出了可以用于reduction子句的一些操作符以及对应私有拷贝变量缺省的初始值,私有拷贝变量的实际初始值依赖于reduction变量的数据类型:+(0)、-(0)、*(1)、&(~0)、|(0)、^(0)、&&(1)、||(0)。如果在并行域内不加锁保护就直接对共享变量进行写操作,存在数据竞争问题,会导致不可预测的异常结果。如果共享数据作为private、firstprivate、lastprivate、threadprivate、reduction子句的参数进入并行域后,就变成线程私有了,不需要加锁保护了。
int i, sum = 100;
#pragma omp parallel for reduction(+:sum)
for (i=0; i&1000; ++i)
printf(&sum=%ld\n&, sum);sum=499600
OpenMP编译器结构:它和所有编译器一样具有相似的结构,即典型的八个部件构成,它们分别是词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成、信息表管理和错误处理。(1)、词法分析程序:词法分析(Lexical analysis)或扫描(Scanning)是编译器最前端的输入模块,它将源代码文件中的一个长长的字符串,逐个识别为有意义的词素(Lexeme)或单词符号,并转变为便于内部处理的格式来保存。通常词法扫码器的工作任务有:识别出源程序中的每个基本语法单位(通常称为单词或语法符号);删除无用的空白字符、回车字符以及其他与语言无直接关系的非实质性字符;删除注释行;进行词法检查并报告所发现的错误。(2)、语法分析程序:语法分析(Syntax
Analysis)或解析(Parsing)程序需要借助于词法分析,将词法分析输出的内部编码格式表示的单词序列尝试构建出一个符合语法规则的完整语法树。如果无法成功建立起一个合法的语法树,则由错误处理模块输出相应的语法错误信息。(3)、语义分析程序(Semantic Analysis):语义特征表征的是各个语法成分的含义和功能,包括这些语法元素的属性或执行时应进行的运算或操作。(4)、中间代码生成:指的是编译器未输出目标代码之前在内部使用的一种源代码的等价表示。(5)、代码优化程序:代码优化是为了提供更高质量目标代码,该工作常常在中间代码生成和目标代码输出之间插入一个代码优化处理的阶段来实现。根据目标代码的目标期望不同,优化方法也相应不同,有的是以运行时间为标准越快越好,有的是以存储空间开销为标准占用内存越少越好。(6)、目标代码生成:是以语义分析(也可能加上优化处理)产生的中间代码作为输入的,它将中间代码翻译为最终形式的目标代码。(7)、信息表管理程序:在编译过程中总是需要收集、记录或查询源程序中出现的各种量的有关属性信息,因此编译程序需要建立和维护多个不同用途的表格(例如常数表、变量名、循环层次等等),这些表格统称为符号表。在编译过程中,造表和查表工作由一系列程序(或函数)来完成,它们并不是独立的存在而是安插在编译程序的相关功能代码中。(8)、错误处理:由于编程人员不可避免的会写出有错误的代码,一个可用的编译器必须能够发现大多数常见错误,并能准确地报告出错误在源代码中的位置,否则就没有使用价值。
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:1879197次
积分:21595
积分:21595
排名:第227名
原创:274篇
转载:168篇
评论:1259条
(3)(12)(10)(5)(8)(4)(1)(3)(7)(7)(3)(2)(8)(2)(2)(8)(9)(10)(4)(4)(4)(5)(4)(4)(6)(9)(12)(1)(4)(11)(4)(5)(4)(1)(1)(3)(3)(2)(1)(2)(1)(1)(2)(3)(4)(1)(2)(2)(4)(2)(4)(2)(6)(2)(5)(4)(7)(12)(9)(1)(14)(17)(11)(15)(23)(4)(11)(20)(15)(18)(17)(2)(1)(2)}

我要回帖

更多关于 预编译语句 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信