OpenMP并行编程学习

OpenMP并行编程学习

主要参考文章:《OpenMP编译原理及实现技术》摘录

一、OpenMP简介

1.什么是OpenMP

OpenMP:Open Multi-Processing
可以说OpenMP制导指令将C语言扩展为一个并行语言,但OpenMP本身不是一种独立的并行语言,而是为多处理器上编写并行程序而设计的、指导共享内存、多线程并行的编译制导指令和应用程序编程接口(API),可在C/C++和Fortran中应用,并在串行代码中以编译器可识别的注释形式出现。OpenMP标准是由一些具有国际影响力的软件和硬件厂商共同定义和提出。

2、OpenMP、MPI、Pthread的区别

2.1、内存系统的不同

OpenMP/Pthread:同一内存访问
在这里插入图片描述
MPI:非同一内存访问
在这里插入图片描述

  • 由于openmp和pthread共享内存,不同线程之间的数据就无须传递,直接传送指针就行
  • 而mpi之间的数据共享需要通过消息传递,因为mpi同步的程序属于不同的进程,甚至不同的主机上的不同进程(mpi不同主机之间的进程协调工作需要安装mpi软件(例如mpich)来完成)
2.2、编译不同

在这里插入图片描述

3、OpenMP执行方式

OpenMP的执行模型采用fork-join的形式,其中fork创建线程或者唤醒已有线程;join即多线程的会合。fork-join执行模型在刚开始执行的时候,只有一个称为“主线程”的运行线程存在。主线程在运行过程中,当遇到需要进行并行计算的时候,派生出线程来执行并行任务。在并行执行的时候,主线程和派生线程共同工作。在并行代码执行结束后,派生线程退出或者阻塞,不再工作,控制流程回到单独的主线程中。
OpenMP线程:在OpenMP程序中用于完成计算任务的一个执行流的执行实体,可以是操作系统的线程也可以是操作系统上的进程。
在这里插入图片描述

4、OpenMP编译模型

#pragma omp指令[子句[子句]…],在C/C++程序中,OpenMP的所有编译制导指令是以#pragma omp开始,后面跟具体的功能指令(或命令)。其中指令或命令是可以单独出现的,而子句则必须出现在制导指令之后
OpenMP编程模型以线程为基础,通过编译制导指令制导并行化,有三种编程要素可以实现并行化控制,他们分别是编译制导API函数集环境变量

4.1、编译制导
4.1.1、指令
  • parallel:用在一个结构块之前,表示这段代码将被多个线程并行执行
  • for:用于for循环语句之前,表示将循环计算任务分配到多个线程中并行执行,以实现任务分担,必须由编程人员自己保证每次循环之间无数据相关性
  • parallel for:parallel和for指令的结合,也是用在for循环语句之前,表示for循环体的代码将被多个线程并行执行,它同时具有并行域的产生和任务分担两个功能
  • sections:用在可被并行执行的代码段之前,用于实现多个结构块语句的任务分担,可并行执行的代码段各自用section指令标出
  • parallel sections:parallel和sections两个语句的结合,类似于parallel for
  • single:用在并行域内,表示一段只被单个线程执行的代码
  • critical:用在一段代码临界区之前,保证每次只有一个OpenMP线程进入
  • flush:保证各个OpenMP线程的数据影像的一致性
  • barrier:用于并行域内代码的线程同步,线程执行到barrier时要停下等待,直到所有线程都执行到barrier时才能继续往下执行
  • atomic:用于指定一个数据操作需要原子性地完成
  • master:用于指定一段代码由主线程执行
  • threadprivate:用于指定一个或多个变量是线程专用
4.1.2、子句
  • private:指定一个或多个变量在每个线程中都有它自己的私有副本
  • firstprivate:指定一个或多个变量在每个线程都有它自己的私有副本,并且私有变量要在进入并行域或认为分担域时,继承主线程中的同名变量的值作为初值
  • lastprivate:是用来指定将线程中的一个或多个私有变量的值在并行处理结束后复制到主线程中的同名变量中,负责拷贝的线程是for或sections任务分担中的最后一个线程
  • reduction:用来指定一个或多个变量是私有的,并且在并行处理结束后这些变量要执行指定的归约运算,并将结果返回给主线程同名变量
  • nowait:指出并发线程可以忽略其他制导指令暗含的路障同步
  • num_threads:指定并行域内的线程的数目
  • schedule:指定for任务分担中的任务分配调度类型
  • shared:指定一个或多个变量为多个线程间的共享变量
  • copyprivate:配合single指令,将指定线程的专有变量广播到并行域内其他线程的同名变量中
  • copyin:用来指定一个threadprivate类型的变量需要用主线程同名变量进行初始化
  • default:用来指定并行域内的变量的使用方式,缺省是shared。
4.2、API函数集

用于控制并发线程的某些行为

  • omp_in_parallel:判断当前是否在并行域中
  • omp_get_thread_num:返回线程号
  • omp_set_num_threads:设置后续并行域中的线程个数
  • omp_get_num_threads:返回当前并行区域中的线程数
  • omp_get_max_threads:获取并行域可用的最大线程数目
  • omp_get_num_procs:返回系统中处理器个数
  • omp_get_dynamic:判断是否支持动态改变线程数目
  • omp_set_dynamic:启用或关闭线程数目的动态改变
  • omp_get_nested:判断系统是否支持并行嵌套
  • omp_set_nested:启用或关闭并行嵌套
  • omp_init(_nest)_lock:初始化一个(嵌套)锁
  • omp_destroy(_nest)_lock:销毁一个(嵌套)锁
  • omp_set(_nest)_lock:(嵌套)加锁操作
  • omp_unset(_nest)_lock:(嵌套)解锁操作
  • omp_test(_nest)_lock:非阻塞的(嵌套)加锁
  • omp_get_wtime:获取wall time时间
  • omp_set_wtime:设置wall time时间
4.3、环境变量

在一定程度上控制OpenMP程序的行为

  • OMP_SCHEDULE:用于for循环并行化后的调度,它的值就是循环调度的类型
  • OMP_NUM_THREADS:用于设置并行域中的线程数
  • OMP_DYNAMIC:通过设定变量值,来确定是否允许动态设定并行域内的线程数
  • OMP_NESTED:指出是否可以并行嵌套

二、OpenMP编程

1、如何在计算机上编译运行OpenMP

(虚拟机)ubuntu下使用OpenMP
第一步:(编译)gcc -fopenmp -o 可执行文件名 文件名
第二步:(运行)./可执行文件名
例如:在这里插入图片描述

(windows)Dev-C++下使用OpenMP:工具栏→工具→编译选项→勾选“编译时加入以下命令”→框框里写入“-fopenmp”→确定

2、制导指令和子句功能上的四大类

(下文大多转载自上述参考文章)

制导指令和子句按照功能可以大体上分成四类:(1)、并行域控制类;(2)、任务分担类;(3)、同步控制类;(4)、数据环境类。并行域控制类指令用于指示编译器产生多个线程以并发执行任务,任务分担类指令指示编译器如何给各个并发线程分发任务,同步控制类指令指示编译器协调并发线程之间的时间约束关系,数据环境类指令处理并行域内外的变量共享或私有属性以及边界上的数据传送操作等。

2.1、并行域控制类

在OpenMP的相邻的fork、join操作之间称之为一个并行域,并行域可以嵌套

2.1.1、 parallel

基本格式:

#pragma omp parallel[for | sections][子句[子句]]
//大括号{}把要并行执行的代码括起来
{ //对应fork
… 代码 …
}//对应join

example1:

#pragma omp parallel
{
	printf("hello, world! Threadid=%d\n", omp_get_thread_num());
}
hello, world! Threadid=1
hello, world! Threadid=3
hello, world! Threadid=2
hello, world! Threadid=5
hello, world! Threadid=4
hello, world! Threadid=6
hello, world! Threadid=0
hello, world! Threadid=7

可以看得出parallel语句中的代码被执行了八次,说明总共创建了8个线程去执行parallel语句中的代码
也可以指定使用多少个线程来执行,需要使用num_threads子句:
example2:

#pragma omp parallel num_threads(4)
{
	printf("hello, world! Threadid=%d\n", omp_get_thread_num());
}
hello, world! Threadid=0
hello, world! Threadid=1
hello, world! Threadid=2
hello, world! Threadid=3

可见,parallel指令是用来产生或唤醒多个线程创建并行域的,并且可以用num_threads子句控制线程数目。parallel域中的每行代码都被多个线程重复执行。

2.2、任务分担类

当使用parellel制导指令产生出并行域之后,如果仅仅是多个线程执行完全相同的任务,那么只是徒增计算工作量而不能达到加速计算的目的,甚至可能相互干扰得到错误结果。因此在产生并行域之后,紧接着的问题就是如何将计算任务在这些线程之间分配,并加快计算结果的产生速度及其保证正确性。OpenMP可以完成的任务分担的指令只有for、sections和single,严格意义上来说只有for和sections是任务分担指令,而single只是协助任务分担的指令。任务分担域和并行域的定义一样,既是指代码区间也是指执行时间区间。

2.2.1、for

for指令指定紧随它的循环语句必须由线程组并行执行,用来将一个for循环任务分配到多个线程,此时各个线程各自分担其中一部分工作。

#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

从结果可以看出四次循环都在一个线程里执行,可见 for指令要和parallel指令结合起来使用才有效果,如下有两种写法:

#pragma omp parallel for
     for ( j = 0; j < 4; j++ ){
         printf( “ j = %d, ThreadId = %d/n ” , j, omp_get_thread_num());
     }

#pragma omp parallel
{
#pragma omp for
     for ( j = 0; j < 4; j++ ){
         printf( “ j = %d, ThreadId = %d/n ” , j, omp_get_thread_num());
     }
}

结果:

j = 1, ThreadId = 1
j = 3, ThreadId = 3
j = 2, ThreadId = 2
j = 0, ThreadId = 0

在一个 parallel 块中也可以有多个for语句,如:

int j;
#pragma omp parallel
{
#pragma omp for
     for ( j = 0; j < 100; j++ ){}
#pragma omp for
     for (  j = 0; j < 100; j++ ){}}

【注】此时只有一个并行域,在该并行域内的多个线程首先完成第一个for语句的任务分担,然后在此进行一次同步(for制导指令本身隐含有结束处的路障同步),然后再进行第二个for语句的任务分担,直到退出并行域只剩下一个主线程为止。

2.2.1.1、for调度

可参考文章:OpenMP的任务调度

当循环中每次迭代的计算量不相等时,如果简单地给各个线程分配相同次数的迭代的话,会使得各个线程计算负载不均衡,这会使得有些线程先执行完,有些后执行完,造成某些CPU核空闲,影响程序性能。在OpenMP的for任务分担中,任务的划分称为调度,各个线程如何划分任务是可以调整的,因此有静态划分、动态划分等,所以调度也分成多个类型。for任务调度子句只能用于for制导指令中。

在OpenMP中,对for循环任务调度使用schedule子句来实现:schedule(type[, size])
type: 参数,表示调度类型,有四种调度类型如下:static、dynamic、guided、runtime
size:size参数为可选,表示以循环迭代次数计算的划分单位,每个线程所承担的计算任务对应于0个或若干个size次循环,size参数必须是整数
【注】static、dynamic、guided三种调度方式都可以使用size参数,也可以不使用size参数。当type参数类型为runtime时,size参数是非法的

2.2.1.2、static静态调度

当for或者parallelfor编译制导指令没有带schedule子句时,大部分系统中默认采用size为1的static调度方式

int i=0;
#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

【注】由于多线程执行时序的随机性,每次执行时打印的结果顺序可能存在差别

int i=0;
#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次连续的迭代计算
cons:负载不均

2.2.1.3、dynamic动态调度

pros:是动态地将迭代分配到各个线程,各线程动态的申请任务,因此较快的线程可能申请更多次数,而较慢的线程申请任务次数可能较少,因此动态调整可以在一定程度上避免前面提到的按循环次数划分引起的负载不平衡问题

int i=0;
 
#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
int i=0;
 
#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

cons:动态调整时,size小有利于实现更好的负载均衡,但是会引起过多的任务动态申请的开销,反之size大则开销较少,但是不易于实现负载平衡,size的选择需要在这两者之间进行权衡

2.2.1.4、guided调度

是一种采用指导性的启发式自调度方法。开始时每个线程会分配到较大的迭代块,之后分配到的迭代块会逐渐递减。迭代块的大小会按指数级下降到指定的size大小,如果没有指定size参数,那么迭代块大小最小会降到1

int i=0;
 
#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.1.5、runtime调度

它不像static、dynamic、guided三种调度方式那样是真实调度方式。它是在运行时根据环境变量OMP_SCHEDULE来确定调度类型,最终使用的调度类型仍然是static、dynamic、guided中的一种
eg.
例如在unix系统中,可以使用setenv命令来设置OMP_SCHEDULE环境变量:
setenv OMP_SCHEDULE “dynamic, 2”
上述命令设置调度类型为动态调度,动态调度的迭代次数为2。
在windows环境中,可以在”系统属性|高级|环境变量”对话框中进行设置环境变量。

2.2.1.6、调度选择

在这里插入图片描述

2.2.2、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临界区

critical临界区:在可能产生内存数据访问竞争的地方,都需要插入相应的临界区制导指令,格式:#pragam omp critical[(name)] critical语句不允许互相嵌套。

int i;
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);

count=80000
由于使用atomic语句,则避免了可能出现的数据访问竞争情况,最后的执行结果都是一致的。而将atomic这一行语句从源程序中删除时,由于有了数据访问的竞争情况,所以最后的执行结果是不确定的。

2.3.3、barrier同步路障

路障(barrier)是OpenMP线程的一种同步方法。线程遇到路障时必须等待,直到并行区域内的所有线程都到达了同一点,才能继续执行下面的代码。在每一个并行域和任务分担域的结束处都会有一个隐含的同步路障,执行此并行域/任务分担域的线程组在执行完毕本区域代码之前,都需要同步并行域的所有线程。也就是说在parallel、for、sections和single构造的最后,会有一个隐式的路障。在有些情况下,隐含的同步路障并不能提供有效的同步措施。这时,需要程序员插入明确的同步路障语句#pragma omp barrier。此时,在并行区域的执行过程中,所有的执行线程都会在同步路障语句上进行同步。

#pragma omp parallel
{
	Initialization();
 
	#pragma  omp barrier
	Process();
}

只有等所有的线程都完成Initialization()初始化操作以后,才能够进行下一步的处理动作,因此,在此处插入一个明确的同步路障操作以实现线程之间的同步。

2.3.4、nowait

为了避免在循环过程中不必要的同步路障并加快运行速度,可以使用nowait子句除去这个隐式的路障。

int i, j;
 
#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从句。

int a[5], i;
 
#pragma omp parallel
{
	#pragma omp for
	for (i=0; i<5; ++i) {
		a[i] = i * i;
	}
 
	#pragma omp master
	for (i=0; i<5; ++i) {
		printf("a[%d]=%d\n", i, a[i]);
	}
}

a[0]=0
a[1]=1
a[2]=4
a[3]=9
a[4]=16

只有一个线程将逐个元素打印出来。

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_t lock;
 
int i;
 
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 +

0 -

0 +

0 -

1 +

1 -

3 +

3 -

2 +

2 -
示例对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

k=1

k=4

k=5

k=2

k=3

k=6

k=7

last k=100

for循环前的变量k和循环区域内的变量k其实是两个不同的变量。用private子句声明的私有变量的初始值在并行域的入口处是未定义的,它并不会继承同名共享变量的值。

2.4.1.4、firstprivate子句

private声明的私有变量不会继承同名变量的值,于是OpenMP提供了firstprivate子句来实现这个功能。firstprivate子句是private子句的超集,即不仅包含了private子句的功能,而且还要对变量做进行初始化。

int i, k=100;
 
#pragma omp parallel for firstprivate(k)
for (i=0; i<4; ++i) {
	k += i;
	printf("k=%d\n", k);
}
 
printf("last k=%d\n", k);

k=100

k=103

k=101

k=102

last k=100

并行域内的私有变量k继承了外面共享变量k的值100作为初始值,并且在退出并行区域后,共享变量k的值保持为100未变。

2.4.1.5、lastprivate子句

lastprivate子句就是用来实现在退出并行域时将私有变量的值赋给共享变量

区别:OpenMP规范中指出,如果是for循环迭代,那么是将最后一次循环迭代中的值赋给对应的共享变量;如果是sections构造,那么是代码中排在最后的section语句中的值赋给对应的共享变量。注意这里说的最后一个section是指程序语法上的最后一个,而不是实际运行时的最后一个运行完的。

int i, k=100;
 
#pragma omp parallel for firstprivate(k), lastprivate(k)
for (i=0; i<4; ++i) {
	k += i;
	printf("k=%d\n", k);
}
 
printf("last k=%d\n", k);

k=101

k=100

k=102

k=103

last k=103

退出for循环的并行区域后,共享变量k的值变成了103,而不是保持原来的100不变。

2.4.1.6、flush
2.4.2、线程专有数据
2.4.2.1、threadprivate子句
2.4.2.2、copyin子句
2.4.2.3、copyprivate子句
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) {
	sum += i;
}
 
printf("sum=%ld\n", sum);

【注】文章末尾仍有不解之处,待续

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值