简介

OpenMP是一种API,用于编写可移植的多线程应用程序,无需程序员进行复杂的线程创建、同步、负载平衡和销毁工作。 使用OpenMP的好处:

  1. CPU核数扩展性问题
  2. 方便性问题
  3. 可移植性问题

OpenMP指令和库函数介绍: 在C/C++中,OpenMP指令使用的格式为

  #pragma omp 指令 [子句[子句]…]
  

并行for循环

 #pragma omp parallel for
        for(i=0;i<length;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); 
        }
 

管理共享数据和私有数据

  1. private:每个线程都拥有该变量的一个单独的副本,可以私有的访问
  2. private:说明列表中的每个变量对于每个线程都应该有一个私有副本。这个私有副本用变量的默认值进行初始化
  3. reduction:
  4. 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环境中,可以在”系统属性|高级|环境变量”对话框中进行设置环境变量。

有效地使用归约

 sum=0;
 for(k=0;k<100;k++)
 {
     sum=sum+func(k);
 }

为了完成这种形式的循环计算,其中的操作必须满足算术结合律和交换律,同时sum是共享的,这样循环内部都可以加给这个变量,同时又必须是私有的,以避免在相加时的数据竞争。

reduction子句可以用来有效地合并一个循环中某些关于一个或多个变量的满足结合律的算术归约操作。reduction子句主要用来对一个或多个参数条目指定一个操作符,每个线程将创建参数条目的一个私有拷贝,在区域的结束处,将用私有拷贝的值通过指定的运行符运算,原始的参数条目被运算结果的值更新。

 sum=0;
 #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);
}
}
 

任务分配区

现实中应用程序的所有性能敏感的部分不是都在一个并行区域内执行,所以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_value;
          }

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='*'"