为了正常的体验网站,请在浏览器设置里面开启Javascript功能!

OpenMP 编程基础

2017-10-01 44页 doc 86KB 17阅读

用户头像

is_888153

暂无简介

举报
OpenMP 编程基础OpenMP 编程基础 OpenMP 编程基础 2014-04-03 11:25:49| 分类: 并行计算|举报|字号 订阅 可以说 OpenMP 制导指令将 C 语言扩展为一个并行语言,但 OpenMP 本身不是一种独立的并行语言,而是为多处理器上编写并行程序而设计的、指导共享内存、多线程并行的编译制导指令和应用程序编程接口(API),可在 C/C++和 Fortran(77、90 和 95)中应用,并在串行代码中以编译器可识别的注释形式出现。OpenMP 标准是由一些具有国际影响力的软件和硬件厂商共同定义和提出,是一...
OpenMP 编程基础
OpenMP 编程基础 OpenMP 编程基础 2014-04-03 11:25:49| 分类: 并行计算|举报|字号 订阅 可以说 OpenMP 制导指令将 C 语言扩展为一个并行语言,但 OpenMP 本身不是一种独立的并行语言,而是为多处理器上编写并行程序而设计的、指导共享内存、多线程并行的编译制导指令和应用程序编程接口(API),可在 C/C++和 Fortran(77、90 和 95)中应用,并在串行代码中以编译器可识别的注释形式出现。OpenMP 标准是由一些具有国际影响力的软件和硬件厂商共同定义和提出,是一种在共享存储体系结构的可移植编程模型,广泛应用于 Unix、Linux、 Windows 等多种平台上。 本章讲述与 C 语言绑定的 OpenMP1并行程序设计的基础知识,包括 OpenMP 基本要素、 编译制导指令(Compiler Directive)、运行库函数(Runtime Library)和环境变量(Environment Variables)。 2.1 OpenMP 基本概念 首先来了解 OpenMP 的执行模式和三大要素。 2.1.1 执行模式 OpenMP 的执行模型采用 fork-join 的形式,其中 fork 创建新线程或者唤醒已有线程;join 即多线程的会合。fork-join 执行模型在刚开始执行的时候,只有一个称为“主线程”的运行线 程存在。主线程在运行过程中,当遇到需要进行并行计算的时候,派生出线程来执行并行任务。 在并行执行的时候,主线程和派生线程共同工作。在并行代码执行结束后,派生线程退出或者阻塞,不再工作,控制流程回到单独的主线程中。 OpenMP 的编程者需要在可并行工作的代码部分用制导指令向编译器指出其并行属性,而且这些并行区域可以出现嵌套的情况,如图 2.1 所示。下面对术语并行域(Paralle region)作如下定义:在成对的 fork 和 join 之间的区域,称为并行域,它既表示代码也表示执行时间区间。对 OpenMP 线程作如下定义:在 OpenMP 程序中用于完成计算任务的一个执行流的执行实体,可以是操作系统的线程也可以是操作系统上的进程。 2.1.2 OpenMP 编程要素 OpenMP 编程模型以线程为基础,通过编译制导指令来显式地指导并行化,OpenMP 为编 程人员提供了三种编程要素来实现对并行化的完善控制。它们是编译制导、API 函数集和环境 变量。 编译制导 在 C/C++程序中,OpenMP 的所有编译制导指令是以#pragma omp 开始,后面跟具体的功能指令(或命令),其具有如下形式: #pragma omp 指令 [子句[, 子句] ?] 支持 OpenMP 的编译器能识别、处理这些制导指令并实现其功能。其中指令或命令是可 句按照功能可以单独出现的,而子句则必须出现在制导指令之后。制导指令和子以大体上分成四类: 1)并行域控制类; 2)任务分担类; 3)同步控制类; 4)数据环境类。 并行域控制类指令用于指示编译器产生多个线程以并发执行任务,任务分担类指令指示编译器如何给各个并发线程分发任务,同步控制类指令指示编译器协调并发线程之间的时序约束关系,数据环境类指令处理并行域内外的变量共享或私有属性以及边界上的数据传送操作等。 下面简单地介绍一下制导指令和相关的子句,现在不必完全弄懂它们的作用,只需大概了解即可,后面还将有详细的使用说明。此处的可以作为学习过程中的速查表。 1)版本为 2.5 的 OpenMP 规范中的指令有以下这些: parallel:用在一个结构块之前,表示这段代码将被多个线程并行执行; for:用于 for 循环语句之前,表示将循环计算任务分配到多个线程中并行执行,以实现任务分担,必须由编程人员自己保证每次循环之间无数据相关性; parallel for:parallel 和 for 指令的结合,也是用在 for 循环语句之前,表示 for循环体的代码将被多个线程并行执行,它同时具有并行域的产生和任务分担两个功能; sections:用在可被并行执行的代码段之前,用于实现多个结构块语句的任务分担,可并行执行的代码段各自用 section 指令标出(注意区分 sections 和 section); parallel sections:parallel 和 sections 两个语句的结合,类似于 parallel for; single:用在并行域内,表示一段只被单个线程执行的代码; critical:用在一段代码临界区之前,保证每次只有一个 OpenMP 线程进入; flush:保证各个 OpenMP 线程的数据映像的一致性; 时要停下等待,直barrier:用于并行域内代码的线程同步,线程执行到 barrier 到所有线程都执行到 barrier 时才继续往下执行; atomic:用于指定一个数据操作需要原子性地完成; master:用于指定一段代码由主线程执行; threadprivate:用于指定一个或多个变量是线程专用(具有持久性),后面会解释线程专有和私有的区别。 2)相应的 OpenMP 的子句有以下一些: private:指定一个或多个变量在每个线程中都有它自己的私有副本; firstprivate:指定一个或多个变量在每个线程都有它自己的私有副本,并且私有变量要在进入并行域或任务分担域时,继承主线程中的同名变量的值作为初值; lastprivate:指定将线程中的一个或多个私有变量的值在并行处理结束后复制到主线程中的同名变量中,负责拷贝的线程是 for 或 sections 任务分担中的最后 一个线程; reduction:用来指定一个或多个变量是私有的,并且在并行处理结束后这些变量要执行指定的归约运算,并将结果返回给主线程同名变量; nowait:指出并发线程可以忽略其他制导指令暗含的路障同步; num_threads:指定并行域内的线程的数目; schedule:指定 for 任务分担中的任务分配调度类型; shared:指定一个或多个变量为多个线程间的共享变量; ordered:用来指定 for 任务分担域内指定代码段需要按照串行循环次序执行; copyprivate:配合 single 指令,将指定线程的专有变量广播到并行域内其他线程的同名变量中; copyin:用来指定一个threadprivate类型的变量需要用主线程同名变量进行初始化; default:用来指定并行域内的变量的使用方式,缺省是 shared。 这些制导指令将会在后面的编程部分进行详细说明解释。 API 函数 除上述编译制导指令之外,OpenMP 还提供了一组 API 函数用于控制并发线程的某些行为, 下面列出 OpenMP 2.5 所有的 API 函数: 表 2.1 OpenMP API 函数 函数名 作用 omp_in_parallel 判断当前是否在并行域中 omp_get_thread_num 返回线程号 omp_set_num_thread‎‎s 设置后续并行域中的线程个数 omp_get_num_threads 返回当前并行区域中的线程‎‎数 omp_get_max_thread‎‎s 获取并行域可用的最大线程数目 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 时间 后面将详细讨论这些函数的用法,表 2.1 可以作为学习中的 API 函数速查表。 环境变量 OpenMP 规范定义了一些环境变量,可以在一定程度上控制 OpenMP 程序的行为。以下是开发过程中常用的环境变量 . OMP_SCHEDULE:用于 for 循环并行化后的调度,它的值就是循环调度的1 类型; 2. OMP_NUM_THREADS:用于设置并行域中的线程数; 3. OMP_DYNAMIC:通过设定变量值,来确定是否允许动态设定并行域内的线程数; 4. OMP_NESTED:指出是否可以并行嵌套。 ICV OpenMP 规范中定义了一些内部控制变量 ICV(Internal Control Variable),用 于表示系统的属性、能力和状态等,可以通过 OpenMP API 函数访问也可以通过环境变量进行修改。但是 变量的具体名字和实现方式可以由各个编译器自行决定。 2.2 OpenMP 编程 下面的内容按照功能进行划分,每个功能部分将可能综合使用编译制导、环境变量和OpenMP API 函数这三种要素。 2.2.1 并行域管理 设计并行程序时,首先需要多个线程来并发地执行任务,因此 OpenMP 编程中第一步就 是应该掌握如何产生出多个线程。如前面所实,在 OpenMP 的相邻的 fork、join 操作之间我们称之为一个并行域,并行域可以嵌套。parallel 制导指令就是用来创建并行域的,它也可以和其他指令如 for、sections 等配合使用形成复合指令。 在 C/C++中,parallel 的使用方法如下: #pragma omp parallel [for | sections] [子句[子句]?] { ...代码... } parallel 语句后面要用一个大括号对将要并行执行的代码括起来。 void main(int argc, char *argv[]) { #pragma omp parallel { 并行域的开始 (对应 fork) printf(“Hello,World!\n”); } 并行域的结束(对应 join) } 执行以上代码将会打印出以下结果 Hello, World! Hello, World! Hello, World! Hello, World! 可以看得出 parallel 语句中的代码被“相同”地执行了 4 次,说明总共创建了 4 个线程来执行 parallel 语句中的代码。为了指定使用多少个线程来执行,可以通过设置环境变 OMP_NUM_THREADS 或者调用 omp_set_num_theads()函数,也可以使用 num_threads 子句, 前者只能在程序刚开始运行时起作用,而 API 函数和子句可以在程序中并行域产生之前起作用。使用 num_threads 子句的例子如下: 1. void main(int argc, char *argv[]) { 2. #pragma omp parallel num_threads(8) 3. { 4. printf(“Hello, World!, ThreadId=%d\n”, omp_get_thread_num() ); 5. } 6. } 执行以上代码,将会打印出以下结果: Hello, World!, ThreadId = 2 Hello, World!, ThreadId = 6 Hello, World!, ThreadId = 4 Hello, World!, ThreadId = 0 Hello, World!, ThreadId = 5 Hello, World!, ThreadId = 7 Hello, World!, ThreadId = 1 Hello, World!, ThreadId = 3 从 ThreadId 的不同可以看出创建了 8 个线程来执行以上代码。所以 parallel 指令是用来产生或唤醒多个线程创建并行域的,并且可以用 num_threads 子句控制线程数目。parallel 域中的每行代码都被多个线程重复执行。和传统的创建线程函数比起来,其过程非常简单直观。 parallel 的并行域内部代码中,若再出现 parallel 制导指令则出现并行嵌套问题,如果设置了 OMP_NESTED 环境变量,那么在条件许可时内部并行域也会由多个线程执行,反之没有设 置 相 应 变 量, 那 么 内 部 并 行 域 的 代 码 将 只 由 一 个线程来执行 。 还有 一 个 环 境 变 量 OMP_DYNAMIC 也影响并行域的行为,如果没有设置该环境变量将不允许动态调整并行域内的线程数目,omp_set_dynamic()也是用于同样的目的。 2.2.2 任务分担 当使用 parellel 制导指令产生出并行域之后,如果仅仅是多个线程执行完全相同的任务,那么只是徒增计算工作量而不能达到加速计算的目的,甚至可能相互干扰得出错误结果。因此 在产生出并行域之后,紧接着的问题就是如何将计算任务在这些线程之间分配,并加快计算结果的生成速度及其保证正确性。OpenMP 可以完成的任务分担的指令只有 for、sections 和 single, 严格意义上来说只有 for 和 sections 是任务分担指令,而 single 只是协助任务分担的指令。 我们对任务分担域定义如下:由 for、sections 或 single 制导指令限定的代码及其执行时间段,也就是说任务分担域和并行域的定义一样,既是指代码区间也是指执行时间区间。 for 制导指令 for 指令指定紧随它的循环语句必须由线程组并行执行,用来将一个 for循环任务分配到多个线程,此时各个线程各自分担其中一部分工作。for 指令一般可以和 parallel 指令合起来形成 parallel for 指令使用,也可以单独用在 parallel 指令的并行域中。 用法如下: #pragma omp [parallel] for [子句] for 循环语句 先看看单独使用 for 语句时是什么效果: 1. int j = 0; 2. #pragma omp for 3. for ( j = 0; j < 4; j++ ){ 4. printf(“j= %d, ThreadId = %d\n”, j, omp_get_thread_num()); 5. } 执行以上代码后打印出以下结果 j = 0, ThreadId = 0 j = 1, ThreadId = 0 j = 2, ThreadId = 0 j = 3, ThreadId = 0 从结果可以看,4 次循环都在一个 ThreadId 为 0 的线程里执行,并没有实现并发执行也不会加快计算速度。可见 for 指令要和 parallel 指令结合起来使用才有效果,即 for 出现在并行域中才能有多个线程来分担任务。以下代码就是 parallel 和 for 一起结合使用的形式: 1. int j = 0; 2. #pragma omp parallel 3. { 4. #pragma omp for 5. for ( j = 0; j < 4; j++ ){ 6. printf(“j= %d, ThreadId = %d\n”, j, omp_get_thread_num()); 7. } 8. } 执行以上代码会打印出以下结果: j = 1, ThreadId = 1 j = 3, ThreadId = 3 j = 2, ThreadId = 2 j = 0, ThreadId = 0 此时,循环计算任务被正确分配到 4 个不同的线程中执行,各自只需要执行一 次循环(总 的串行循环次数为 4,4 个线程每个线程承担 1/4,即 1 次)即可。 上面段代码也可以改写成以下 parallel for 复合制导指令的形式: 1. int j = 0; 2. #pragma omp parallel for 3. for ( j = 0; j < 4; j++ ){ 4. printf(“j= %d, ThreadId = %d\n”, j, omp_get_thread_num()); 5. } 如果并行域中有 4 个线程,执行后会打印出相同的结果: j = 0, ThreadId = 0 j = 2, ThreadId = 2 j = 1, ThreadId = 1 j = 3, ThreadId = 3 现在考虑另一个情况,在一个 parallel并行域中,可以有多个 for 制导指令,如: 1. int j; 2. #pragma omp parallel 3. { 4. #pragma omp for 5. for ( j = 0; j < 100; j++ ){ 6. … 7. } 8. #pragma omp for 9. for ( j = 0; j < 100; j++ ){ 10. … 11. } 12. ? 13. } 此时只有一个并行域,在该并行域内的多个线程首先完成第一个 for 语句的任务分担,然 后在此进行一次同步(for制导指令本身隐含有结束处的路障同步),然后再进行第二个 for 语句的任务分担,直到退出并行域并只剩下一个主线程为止。 for 调度 在 OpenMP 的 for 任务分担中,任务的划分称为调度,各个线程如何划分任务是可以调整 的,因此有静态划分、动态划分等,所以调度也分成多个类型。for 任务调度子句只能用于 for 制导指令中,下面先来看看提供多种调度类型的必要性。 当循环中每次迭代的计算量不相等时,如果简单地给各个线程分配相同次数的迭代的话, 会使得各个线程计算负载不均衡,这会使得有些线程先执行完,有些后执行完,造成某些 CPU 核空闲,影响程序性能。例如: 1. int i, j; 2. int a[100][100] = {0}; 3. for ( i =0; i < 100; i++) 4. { 5. for( j = i; j < 100; j++ ) 6. a[i][j] = i*j; 7. } 如果将最外层循环并行化的话,比如使用 4 个线程,如果给每个线程平均分配 25 次循环 迭代计算的话,显然 i,0 和 i,99 的计算量相差了 100 倍,那么各个线程间可能出现较大的负载不平衡情况。为了解决这些问题,适应不同的计算类型,OpenMP 中提供了几种对 for 循环 并行化的任务调度。 在 OpenMP 中,对 for 循环任务调度使用 schedule 子句来实现,下面介绍 schedule 子句的 用法。 1) schedule 子句用法 schedule 子句的使用格式为: schedule (type [, size]) schedule 有两个参数:type 和 size,size 参数是可选的。如果没有指定 size 大小,循环迭代会尽可能平均地分配给每个线程。 ? type 参数 表示调度类型,有四种调度类型如下: ? static ? dynamic ? guided ? runtime 这四种调度类型实际上只有 static、dynamic、guided 三种调度方式。runtime 实际上是根 据环境变量 OMP_SCHEDULED来选择前三种中的某种类型,相应的内部控制变量 ICV 是 run-sched-var。 ? size 参数 (可选) size 参数表示以循环迭代次数计算的划分单位,每个线程所承担的计算任务对应于 0 个或 若干个 size 次循环,size 参数必须是整数。static、dynamic、guided 三种调度方式都可以使用 size 参数,也可以不使用 size 参数。当 type 参数类型为 runtime 时,size 参数是非法的(不需 要使用,如果使用的话编译器会报错)。 2)static 静态调度 当 for 或者 parallel for 编译制导指令没有带 schedule 子句时,大部分系统中默认采用 size 为 1 的 static 调度方式,这种调度方式非常简单。假设有 n 次循环迭代,t 个线程,那么给每个线程静态分配大约 n/t 次迭代计算。这里为什么说大约分配 n/t 次呢,因为 n/t 不一定是整数,因此实际分配的迭代次数可能存在差 1 的情况,如果指定了 size 参数的话,那么可能相差 size 次迭代。 静态调度时可以不使用 size 参数,也可以使用 size 参数。 ? 不使用 size 参数 不使用 size 参数时,分配给每个线程的是 「n/t」次连续的迭代,不使用 size 参数的用法如下: schedule(static) 例如以下代码: 1. #pragma omp parallel for schedule(static) 2. for(i = 0; i < 10; i++ ) 3. { 4. printf("i=%d, thread_id=%d\n", i, omp_get_thread_num()); 5. } 假设并行域中有两个线程,上面代码执行时打印的结果如下: i=0, thread_id=0 i=1, thread_id=0 i=3, thread_id=0 i=4, thread_id=0 i=5, thread_id=1 i=6, thread_id=1 i=7, thread_id=1 i=8, thread_id=1 i=9, thread_id=1 可以看出线程 0 得到了 i=0,4 的连续迭代,线程 1 得到 i=5,9 的连续迭代。注意由于多线程执行时序的随机性,每次执行时打印的结果顺序可能存在差别, 后面的例子也一样。 ? 使用 size 参数 使用 size 参数时,分配给每个线程的 size 次连续的迭代计算,用法如下: schedule(static, size) 例如以下代码: 1. #pragma omp parallel for schedule(static, 2) 2. for(i = 0; i < 10; i++ ) 3. { 4. printf("i=%d, thread_id=%d\n", i, omp_get_thread_num()); 5. } 若使用两个线程的并行域,执行时会打印以下结果: i=0, thread_id=0 i=1, thread_id=0 i=4, thread_id=0 i=5, thread_id=0 i=8, thread_id=0 i=9, thread_id=0 i=2, thread_id=1 i=3, thread_id=1 i=6, thread_id=1 i=7, thread_id=1 从打印结果可以看出,0、1 次迭代分配给 0 号线程,2、3 次迭代分配给 1 线程号,4、5 次迭代分配给 0 号线程,6、7 次迭代分配给 1 号线程,?。每个线程依次分配到 2 次(即 size) 连续的迭代计算。 3)dynamic 动态调度 动态调度是动态地将迭代分配到各个线程,动态调度可以使用 size 参数也可以不使用 size 参数,不使用 size 参数时是根据各个线程的完成情况将迭代逐个地分配到各个线程,使用 size 参数时,每次分配给线程的迭代次数为指定的 size 次。各线程动态的申请任务,因此较快的线程可能申请更多次数,而较慢的线程申请任务次数可能较少,因此动态调度可以在一定程度上避免前面提到的按循环次数划分引起的负载不平衡问题。 ?下面为使用动态调度不带 size 参数的例子: 1. #pragma omp parallel for schedule(dynamic) 2. for(i = 0; i < 10; i++ ) 3. { 4. printf("i=%d, thread_id=%d\n", i, omp_get_thread_num()); 5. } 如果并行域使用两个线程,打印结果如下: i=1, thread_id=1 i=2, thread_id=0 i=3, thread_id=1 i=5, thread_id=1 i=6, thread_id=1 i=7, thread_id=1 i=8, thread_id=1 i=4, thread_id=0 i=9, thread_id=1 由于没有指定 size 所以任务划分是按 1次迭代进行的。 ?下面为动态调度使用 size 参数的例子: 1. #pragma omp parallel for schedule(dynamic, 2) 2. for(i = 0; i < 10; i++ ) 3. { 4. printf("i=%d, thread_id=%d\n", i, omp_get_thread_num()); 5. } 打印结果如下: i=0, thread_id=0 i=1, thread_id=0 i=4, thread_id=0 i=2, thread_id=1 i=5, thread_id=0 i=3, thread_id=1 i=6, thread_id=0 i=8, thread_id=1 i=7, thread_id=0 i=9, thread_id=1 从打印结果可以看出第“0、1”,“4、5”,“6、7”次迭代被分配给了线程 0,第“2、 3”,“8、9”次迭代则分配给了线程 1,每次分配的迭代次数为 2。较快的线程“抢到”了 更多的任务。 动态调度时,size 小有利于实现更好的负载均衡,但是会引起过多的任务动态申请的开销, 反之 size 大则开销较少,但是不易于实现负在平衡,size 的选择需要在这两者之间进行权衡。 4) guided 调度 guided 调度是一种采用指导性的启发式自调度方法。开始时每个线程会分配到较大的迭 代块,之后分配到的迭代块会逐渐递减。迭代块的大小会按指数级下降到指定的 size大小, 如果没有指定 size 参数,那么迭代块大小最小会降到 1。 例如以下代码: 1. #pragma omp parallel for schedule(guided,2) 2. for (i = 0; i < 10; i++ ) 3. { 4. printf("i=%d, thread_id=%d\n", i, omp_get_thread_num()); 5. } 打印结果如下: i=0, thread_id=0 i=1, thread_id=0 i=2, thread_id=0 i=3, thread_id=0 i=8, thread_id=0 i=9, thread_id=0 i=5, thread_id=1 i=6, thread_id=1 i=7, thread_id=1 第 0、1、2、3、4 次迭代被分配给线程 0,第 5、6、7 次迭代被分配给线程 1,第 8、9 次迭代被分配给线程 0,分配的迭代次数呈递减趋势,最后一次递减到 2 次。 5) runtime 调度 runtime 调度并不是像前面三种调度方式那样是真实调度方式,它是在运行时根据环境变 量 OMP_SCHEDULE 来确定调度类型,最终使用的调度类型仍然是上述三种调度方式中的一种。 例如在 unix 系统中,可以使用 setenv 命令来设置 OMP_SCHEDULE 环境变量: setenv OMP_SCHEDULE “dynamic,2” 如果程序中选择 runtime 调度,那么上述命令设置调度类型为动态调度,动态调度的 size 为 2。在 windows 环境中,可以在“系统属性|高级|环境变量”对话框中进行设置环境变量。 sections 制导指令 sections 编译制导指令是用于非迭代计算的任务分担,它将 sections 语句里的代码用 section 制导指令划分成几个不同的段(可以是一条语句,也可以是用{...}括起来的结构块),不同的 section 段由不同的线程并行执行。用法如下: #pragma omp [parallel] sections [子句] { #pragma omp section { ...代码块... } [#pragma omp section] ... } 先看一下以下具有三个 section 的例子代码: 1. void main(int argc, char *argv) 2. { 3. #pragma omp parallel sections { 4. #pragma omp section 5. printf(“section 1 thread = %d\n”, omp_get_thread_num()); 6. #pragma omp section 7. printf(“section 2 thread = %d\n”, omp_get_thread_num()); 8. #pragma omp section 9. printf(“section 3 thread = %d\n”, omp_get_thread_num()); 10. } 执行后将打印出以下结果: section 1 thread = 0 section 2 thread = 2 section 3 thread = 1 此时,各个 section 里的代码是被分配到不同的线程并发地执行。下面来看看在一个并行域内有多个 sections 的情况: 1. void main(int argc, char *argv) 2. { 3. #pragma omp parallel 4. { 5. #pragma omp sections 6. { 7. #pragma omp section 8. printf(“section 1 ThreadId = %d\n”, omp_get_thread_num()); 9. #pragma omp section 10. printf(“section 2 ThreadId = %d\n”, omp_get_thread_num()); 11. } 12. #pragma omp sections 13. { 14. #pragma omp section 15. printf(“section 3 ThreadId = %d\n”, omp_get_thread_num()); 16. #pragma omp section 17. printf(“section 4 ThreadId = %d\n”, omp_get_thread_num()); 18. } 19. } 20. } 执行后将打印出以下结果: section 1 ThreadId = 0 section 2 ThreadId = 3 section 3 ThreadId = 3 section 4 ThreadId = 1 这种方式和前面那种方式的区别是,这里有两个 sections 构造先后串行执行的,即第二个 sections 构造的代码要等第一个 sections 构造的代码执行完后才能执行。sections 构造里面的各个 section 部分代码是并行执行的。与 for 制导指令一样,在 sections 的结束处有一个隐含的路 障同步,没有其他说明的情况下,所有线程都必须到达该点才能往下运行。 使用 section 指令时,需要注意的是这种方式需要保证各个 section 里的代码执行时间相差不大,否则某个 section 执行时间比其他 section 过长就造成了其它线程空闲等待的情况。用 for 语句来分担任务时工作量由系统自动划分,只要每次循环间没有时间上的差距,那么分摊是比较均匀的,使用 section 来划分线程是一种手工划分工作量的方式,最终负载均衡的好坏得依赖于程序员。 single 制导指令 单线程执行 single 制导指令指定所包含的代码段只由一个线程执行,别的线程跳过这段代码。如果没有 nowait 从句,所有线程在 single 制导指令结束处隐式同步点同步。如果 single 制导指令有 nowait 从句,则别的线程直接向下执行,不在隐式同步点等待;single 制导指令用 在一段只被单个线程执行的代码段之前,表示后面的代码段将被单线程执行,具体用法如下: #pragma omp single [子句] 对于如下范例代码: 1. #include 2. int main(int argc,void **argv ) 3. { 4. #pragma omp parallel 5. { 6. #pragma omp single 7. printf (“Beginning work1.\n”); 8. printf(“workon1 parallelly. %d\n”,omp_get_thread_num()); 9. #pragma omp single 10. printf (“Finishing work1.\n”); 11. #pragma omp single nowait 12. printf (“Beginning work2.\n”); 13. printf(“workon2 parallelly. %d\n”,omp_get_thread_num());; 14. } 15. } 对应的输出结果如下: Beginning work1. work on 1 parallelly. 0 work on 1 parallelly. 3 work on 1 parallelly. 2 work on 1 parallelly. 1 Finishing work1. Beginning work2. work on 2 parallelly. 1 work on 2 parallelly. 3 work on 2 parallelly. 2 work on 2 parallelly. 0 从上面的结果可以看出,在并行域内有多个线程并发执行,因此“workon1/2 parallel” 将由 4 个线程并发执行,但是对于使用 single 语句制导的“Beginningwork1/2” 以及“Finishingwork1”的打印语句只有一个线程在执行。 另一种需要使用 single 制导指令的情况是为了减少并行域创建和撤销的开销,而将多个临近的 parallel 并行域合并时。经过合并后,原来并行域之间的串行代码也将被并行执行,违反了代码原来的目的,因此这部分代码可以用 single 指令加以约束只用一个线程来完成。 2.2.3 同步 等语句进行任务分担后,还须考虑的是这在正确产生并行域并用 for、sections 些并发线程的同步互斥需求。在 OpenMP 应用程序中,由于是多线程执行,所以必须有线程互斥机制以保证程序在出现数据竞争的时候能够得出正确的结果,并且能控制线程执行的先后制约关系,以保证执行结果的正确性。 OpenMP 支持两种不同类型的线程同步机制,一种是互斥锁的机制,可以用来保护一块共享的存储空间,使任何时候访问这块共享内存空间的线程最多只有一个,从而保证了数据的完整性;另外一种同步机制是事件同步机制,这种机制保证了多个线程制之间的执行顺序。互斥的操作针对需要保护的数据而言,在产生了数据竞争的内存区域加入互斥,可以使用包括 critical、atomic等制导指令以及 API 中的互斥函数。而事件机制则控制线程执行顺序,包括 barrier 同步路障、ordered 定序区段、matser 主线程执行等。 critical 临界区 在可能产生内存数据访问竞争的地方,都需要插入相应的临界区制导指令,临界区编译制导指令的格式如下: #pragam omp critical [(name)] { 需保护的代码段 } 其中的名字 name 不是必需的。例如以下代码: 1. int i; int max_num_x=max_num_y=-1; 2. #pragma omp parallel for 3. for (i=0; i max_num_x) 7. max_num_x = arx[i]; 8. #pragma omp critical (max_ary) 9. If (ary[i] > max_num_y) 10. max_num_y = ary[i]; 11. } 在一个并行域内的 for 任务分担域中,各个线程逐个进入到 critical 保护的区域内,比较当前元素和最大值的关系并可能进行最大值的更替,从而避免了数据竞争的情况。critical 语句不允许互相嵌套。 atomic 原子操作 在 OpenMP 的程序中,原子操作的功能是通过#pragma omp atomic 编译制导指令提供的。 前面提到的 critical 临界区操作能够作用在任意大小的代码块上,而原子操作只能作用在单条赋值语句中。在 C/C++语言中,原子操作的语法格式如下所示: #pragma omp atomic x =expr 或者 #pragma omp atomic x++ // or x--, --x, ++x 下面是在 C/C++语言中可用的原子操作。 “+*- /&^|<<>>” 值得注意的是,当对一个数据进行原子操作保护的时候,就不能对数据进行临界区的保护,OpenMP 运行时并不能在这两种保护机制之间建立配合机制。用户在针对同一个内存单元使用原子操作的时候,需要在程序所有涉及到该变量并行赋值的部位都加入原子操作的保护。 1. int counter=0; 2. #pragma omp parallel 3. { 4. for(int i=0;i<10000;i++) 5. { 6. #pragma omp atomic //atomic operation 7. counter++; 8. } 9. } 10. printf(“counter = %d\n”, counter); 由于使用 atomic 语句,则避免了可能出现的数据访问竞争情况,最后的执行结果都是一 致的,执行结果总是为下面的数值(假设有两个并发线程): counter=20000 而将 atomic 这一行语句从源程序中删除时,由于有了数据访问的竞争情况,所以最后的 执行结果是不确定的。下面是一个可能的执行结果: counter=12014 该结果因数据竞争而出错。 barrier 同步路障 路障(barrier)是 OpenMP 线程的一种同步方法。线程遇到路障时必须等待,直到并行区域内的所有线程都到达了同一点,才能继续执行下面的代码。在每一个并行域和任务分担域的 结束处都会有一个隐含的同步路障,执行此并行域/任务分担域的线程组在执行完毕本区域代码之前,都需要同步并行域的所有线程。也就是说在 parallel、for、sections 和 single 构造的最 后,会有一个隐式的路障。 在有些情况下,隐含的同步路障并不能提供有效的同步措施。这时,需要程序员插入明确 的同步路障语句#pragma omp barrier。此时,在并行区域的执行过程中,所有的执行线程都会 在同步路障语句上进行同步。 1. #pragma omp parallel 2. { 3. Initialization(); 4. #pragma omp barrier 5. Process(); 6. } 上述例子中,只有等所有的线程都完成 Initialization()初始化操作以后,才能够进行下一步的处理动作,因此,在此处插入一个明确的同步路障操作以实现线程之间的同步。 nowait 为了避免在循环过程中不必要的同步路障并加快运行速度,可以使用 nowait 子句除去这 个隐式的路障。如下范例所示: 1. #include . 2. int main() 3. { 4. int i,j; 5. #pragma omp parallel num_threads(4) 6. { 7. #pragma omp for nowait 8. for (i = 0; i < 8; ++i) 9. { printf("+\n"); } 10. #pragma omp for 11. for (j = 0; j < 8; ++j) 12. { printf(" -\n"); } 13. } 14. return 0; 15. } 执行结果如下: + + + + + + + + - - - - - - - - 也就是说第一个 for 循环结束后,后一个 for 循环才开始。但是使用了 nowait 之后则出现 以下结果: + + - - + + - - + + - - + + - - 此时线程在完成第一个 for 循环子任务后,并不需要同步等待,而是直接执行后 面的任务, 因此出现“-”在“+”前面的情况。 nowait 子句消除了不必要的同步开销,加快了计算速度,但是也引入了实现上 的困难, 这个将在后面编译器相关的章节中讨论。 master 主线程执行 用于指定一段代码由主线程执行。master 制导指令和 single 制导指令类似,区别在于, master 制导指令包含的代码段只由主线程执行,而 single 制导指令包含的代码段可由任一线程 执行,并且 master 制导指令在结束处没有隐式同步, 也不能指定 nowait 从句。语句格式如下: #pragma omp master 下面是一个计算 0~4 的平方数的程序: 1. #include 2. #include 3. int main( ) 4. { 5. int a[5], i; 6. #pragma omp parallel 7. { 8. #pragma omp for 9. for (i = 0; i < 5; i++) 10. a[i] = i * i; 11. #pragma omp master 12. for (i = 0; i < 5; i++) 13. printf_s("a[%d] = %d\n", i, a[i]); 14. } 15. } 结果为: a[0] = 0 a[1] = 1 a[2] = 4 a[3] = 9 a[4] = 16 虽然上面的打印语句是在 parallel 并行域中,但是并没有被多个线程所执行,而是只有一 个线程将逐个元素打印出来。 ordered 顺序制导指令 对于循环代码的任务分担中,某些代码的执行需要按规定的顺序执行。典型的情况如下: 在一次循环的过程中大部分的工作是可以并行执行的,而特定部分代码的工作需要等到前面的工作全部完成之后才能够执行。这时,可以使用 ordered 子句使特定的代码按照串行循环的次序来执行。下面例子说明 ordered 子句是如何对结果产生影响的。 1. #include 2. #include 3. int main( ) 4. { 5. int i; 6. #pragma omp parallel 7. { 8. #pragma omp for ordered 10. for (i = 0; i < 5; i++) #pragma omp ordered 11. printf("iteration %d\n", i); 12. } 13. } 打印结果如下: iteration 0 iteration 1 iteration 2 iteration 3 iteration 4 从结果可以看出,虽然在 ordered 子句之前的工作是并行执行的,但是在遇到 ordered 子 句的时候,只有前面的循环都执行完毕之后,才能够进行下一步执行。上面的例子中将所有循 环迭代都串行化了,实际上可以只将关键部分串行化,其代码框架如下: 14. #pragma omp parallel 15. { 16. #pragma omp for 17. for (i = 0; i < 100; i++) 18. { 19. 一些无数据相关、可并行乱序执行的操作 20. ….. 21. #pragma omp ordered 22. 一些有数据相关、只能顺序执行的操作 23. } 24. } 这样一来,有些任务在并行执行,对于部分必须串行执行的部分才启用 ordered 保护。 互斥锁函数 前面分别介绍了互斥同步的两种方法:atomic 和 critical,除了上述的编译制导指令,OpenMP 还可以通过库函数支持实现互斥操作,方便用户实现特定的同步需求。编译制导指令的互斥支持只能放置在一段代码之前,作用在这段代码之上。而 OpenMP API 所提供的互斥函数可放在任意需要的位置。程序员必须自己保证在调用相应锁操作之后释放相应的锁,否则就可能造成多线程程序的死锁。 表 2.1 中包含 OpenMP API 函数提供的互斥函数和可嵌套的互斥锁函数。 下面来看看互斥函数的使用例子: 1. #include 2. static omp_lock_t lock; 3. int main() 4. { 5. int i; 6. omp_init_lock(&lock); 7. #pragma omp parallel for 8. for (i = 0; i < 5; ++i) 9. { 10. omp_set_lock(&lock); 11. printf("%d +\n",omp_get_thread_num()); 12. printf("%d -\n",omp_get_thread_num()); 13. omp_unset_lock(&lock); 14. } 15. omp_destroy_lock(&lock); 16. return 0; 17. } 下面是其中一个正常运行的结果: 2 + 2 - 3 + 3 - 1 + 1 - 0 + 0 - 4 + 4 - 上边的示例对 for 循环中的所有内容进行加锁保护,同时只能有一个线程执行 for 循环中的内容。因此同一个线程的两次打印之间不会被打断。如果删除代码 中的获得锁、释放锁的代码,因两条打印语句之间间隔太短,大多数情况下也是 正确的,但偶尔可能输出如下错误结果: 3 + 3 - 1 + 1 - 4 + 4 - 2 + 0 + 0 - 2 - 互斥锁函数中只有 omp_test_lock 函数是带有返回值的,该函数可以看作是 omp_set_lock 的非阻塞版本。 2.2.4 数据环境控制 现在读者已经能成功创建并行域生成多个线程、使用任务分担指令并行执行任务、利用同 步 指令 控 制并发 执行中的 互斥和 顺序后 ,还需要 了解的 是如何 使用数据 环境( Data Enviornment)控制指令。由于是多线程环境,因此就涉及了共享变量和私有变量的两个基本问题,在此基础之上还有线程专有数据、变量的初值和终值的设定、归约操作相关的变量等问题。 通常来说 OpenMP 是建立在共享存储结构的计算机之上,使用操作系统提供的 线程作为 并发执行的基础,所以线程间的全局变量和静态变量是共享的,而局部变量、自动变量是私有的。但是对 OpenMP 编程而言,缺省变量往往是共享变量,而不管它是不是全局静态变量还是局部自动变量。也就是说 OpenMP 各个线程的变量是共享还是私有,是依据 OpenMP 自身的规则和相关的数据子句而定,而不是依据操作系统线程或进程上的变量特性而定。 OpenMP 的数据处理子句包括 private、firstprivate、lastprivate、shared、default、reduction copyin 和 copyprivate。它与编译制导指令 parallel,for 和 sections 相结合,用来控制变量的作用范围。它们控制数据环境,比如,哪些串行部分中的数据变量被传递到程序的并行部分以及如何传送,哪些变量对所有并行部分的线程是可见的,哪些变量对所有并行部分的线程是私有的,等等。 共享与私有化 1)shared 子句 shared 子句用来声明一个或多个变量是共享变量。用法如下: shared(list) 需要注意的是,在并行域内使用共享变量时,如果存在写操作,必须对共享变量加以保护, 否则不要轻易使用共享变量,尽量将共享变量的访问转化为私有变量的访问。循环迭代变量在 循环构造的任务分担域里是私有的。声明在任务分担域内的自动变量都是私有的。 2) default 子句 default 子句用来允许用户控制并行区域中变量的共享属性。用法如下: default(shared | none) 使用 shared 时,缺省情况下,传入并行区域内的同名变量被当作共享变量来处理,不会 产生线程私有副本,除非使用 private 等子句来指定某些变量为私有的才会产生 副本。 如果使用 none 作为参数,除了那些由明确定义的除外,线程中用到的变量都必须显式指 定为是共享的还是私有的。 3) private 子句 private 子句用于将一个或多个变量声明成线程私有的变量,变量声明成私有变量后,指 定每个线程都有它自己的变量私有副本,其他线程无法访问私有副本。即使在并行域外有同名 的共享变量,共享变量在并行域内不起任何作用,并且并行域内不会操作到外面的共享变量。 private 子句的用法格式如下: private (list) 下面便是一个使用 private 子句的代码例子: 1. int k = 100; 2. #pragma omp parallel for private(k) 3. for ( k=0; k < 10; k++) 4. { 5. printf("k=%d\n", k); 6. } 7. printf("last k=%d\n", k); 上面程序执行后打印的结果如下: k=6 k=7 k=8 k=9 k=0 k=1 k=2 k=3 k=4 k=5 last k=100 从打印结果可以看出,for 循环前的变量 k 和循环区域内的变量 k 其实是两个不同的变量。 用 private 子句声明的私有变量的初始值在并行域的入口处是未定义的,它并不会继承同名共享变量的值。注意,出现在 reduction 子句中的变量不能出现在 private 子句中。 4) firstprivate 子句 私有变量的初始化和终结操作:OpenMP 编译制导指令需要对这种需求给予支持,即使用 firstprivate 和 lastprivate 来满足这两种需求。使得并行域或任务分担域开始执行时,私有变量 通过主线程中的变量初始化,也可以在并行域或任务分担域结束时,将最后一次一个线程上的 私有变量赋值给主线程的同名变量。 private 声明的私有变量不会继承同名变量的值,于是 OpenMP 提供了 firstprivate 子句来 实现这个功能。firstprivate 子句是 private 子句的超集,即不仅包含了 private 子句的功能,而且还要对变量做进行初始化。其格式如下: firstprivate (list) 先看一下以下的代码例子 1. int k =100; 2. #pragma omp parallel for firstprivate(k) 3. for ( i=0; i < 4; i++) 4. { 5. k += i; 6. printf("k=%d\n",k); 7. } 8. printf("last k=%d\n", k); 上面代码执行后打印结果如下: k=100 k=101 k=103 k=102 last k=100 从打印结果可以看出,并行域内的私有变量 k 继承了外面共享变量 k 的值 100 作为初始值, 并且在退出并行区域后,共享变量 k 的值保持为 100 未变。 5) lastprivate 子句 有时要将任务分担域内私有变量的值经过计算后,在退出时, 将它的值赋给同名的共享变量(前面的 private 和 firstprivate 子句在退出并行域时都没有将私有变量的最后取值赋给对应的共享变量),lastprivate 子句就是用来实现在退出并行域时将私有变量的值赋给共享变量。 lastprivate 子句也是 private 子句的超集,即不仅包含了 private 子句的功能,而且还要将变量 从 for、sections 的任务分担域中最后的线程中复制给外部同名变量。其格式如下: lastprivate (list) 举个例子如下: 1. int k = 100; 2. #pragma omp parallel for firstprivate(k),lastprivate(k) 3. for ( i=0; i < 4; i++) 4. { 5. k+=i; 6. printf("k=%d\n",k); 7. } 8. printf("last k=%d\n", k); 上面代码执行后的打印结果如下: k=100 k=101 k=103 k=102 last k=103 从打印结果可以看出,退出 for 循环的并行区域后,共享变量 k 的值变成了 103,而不是 保持原来的 100 不变。 由于在并行域内是多个线程并行执行的,最后到底是将那个线程的最终计算结果赋给了对应的共享变量呢,OpenMP 规范中指出,如果是 for 循环迭代,那么是将最后一次循环迭代中的值赋给对应的共享变量;如果是 sections 构造,那么是代码中排在最后的 section 语句中的值赋给对应的共享变量。注意这里说的最后一个 section 是指程序语法上的最后一个,而不是 实际运行时的最后一个运行完的。 如果是类(class)类型的变量使用在 lastprivate 参数中,那么使用时有些限制, 需要一个可访问的,明确的缺省构造函数,除非变量也被使用作为 firstprivate 子句的参数;还需要一个拷贝赋值操作符,并且这个拷贝赋值操作符对于不同对象的操作顺序是未指定的,依赖于编译器的定义。 5)flush OpenMP 的 flush 制导指令主要解决多个线程之间的共享变量的一致性问题。用法如下: flush [(list)] 该指令将列表中的变量执行 flush 操作,直到所有变量都已完成相关操作后才返回,保证后续变量访问的一致性。 线程专有数据 线程专有数据和私有数据不太相同,threadprivate 子句用来指定全局的对象被各个线程各自复制了一个私有的拷贝,即各个线程具有各自私有、线程范围内的全局对象。private 变量在退出并行域后则失效,而 threadprivate 线程专有变量可以在前后多个并行域之间保持连续 性。 1) threadprivate 子句 用法如下: #pragma omp threadprivate(list) new-line 下面用 threadprivate 命令来实现一个线程私有的计数器,各个线程使用同一个函数来实现自己的计数。计数器代码如下: 1. int counter = 0; 2. #pragma omp threadprivate (counter) 3. int increment_counter() 4. { 5. counter++; 6. return(counter); 7. } 如果是静态变量也同样可以使用 threadprivate 声明成线程私有的,上面的 counter 变量如 改成用 static 类型来实现时,代码如下: 1. int increment_counter2() 2. { 3. static int counter = 0; 4. #pragma omp threadprivate (counter) 5. counter++; 6. return(counter); 7. } 用作 threadprivate 的变量的地址不能是常数。对于 C++的类(class)类型变量,用作 threadprivate 的参数时有些限制,当定义时带有外部初始化则必须具有明确的拷贝构造函数。对于 windows 系统,threadprivate 不能用于动态装载(使用 windows LoadLibrary 装载)的 DLL 中,可 以用于静态装载的 DLL 中,关于 系统中的更多限制,请参阅 MSDN 中有关 threadprivate 子句的帮助材料。有关 threadprivate 指令的更多限制方面的信息,详情请参阅 OpenMP2.5 规 范。 表 2.2 private 和 threadprivate 区别 Private Threadprivate 数据类型 变量 变量 位置 在域的开始或共享任务单元 在块或整个文件区域的例程的定 义上 持久性 否 是 扩充性 只是词法的-除非作为子程序的参数动态的 而传递 初始化 使用 firstprivate‎‎ 使用 copyin 2) copyin 子句 copyin子句用来将主线程中 threadprivate 变量的值复制到执行并行域的各个线程的threadprivate 变量中,便于所有线程访问主线程中的变量值,其格式如下: copyin (list) copyin 中的参数必须被声明成 threadprivate 的,对于 class 类型的变量,必须带有明确的 拷贝赋值操作符。 对于前面 threadprivate 中讲过的计数器函数,如果多个线程使用时,各个线程都需要对全局变量 counter 的副本进行初始化,可以使用 copyin 子句来实现,示例代码如下: 1. int main(int argc, char* argv[]) 2. { 3. int iterator; 4. #pragma omp parallel sections copyin(counter) 5. { 6. #pragma omp section 7. { 8. int count1; 9. for ( iterator = 0; iterator < 100; iterator++ ) 10. count1 = increment_counter(); 11. printf("count1 = %ld\n", count1); 12. } 13. #pragma omp section 14. { 15. int count2; 16. for ( iterator = 0; iterator < 200; iterator++ ) 17. count2 = increment_counter(); 18. printf("count2 = %ld\n", count2); 19. } 20. } 21. printf("counter = %ld\n", counter); 22. } 打印结果如下: count1 = 100 count2 = 200 counter = 0 从打印结果可以看出,两个线程都正确实现了各自的计数,而外部共享变量仍为 0。 2) copyprivate 子句 copyprivate 子句提供了一种机制,即将一个线程私有变量的值广播到执行同一并行域的其他线程。用法如下: copyprivate(list) copyprivate 子句可以关联 single 构造,在 single 构造的 barrier 到达之前就完成了广播工作。copyprivate 可以对 private 和 threadprivate 子句中的变量进行操作,但是当使用 single 构 造时,copyprivate 的变量不能用于firstprivate 子句中。 下面便是一个使用 copyprivate 的代码例子: 1. int counter = 0; 2. #pragma omp threadprivate(counter) 3. int increment_counter() 4. { 5. counter++; 6. return(counter); 7. } 8. #pragma omp parallel 9. { 10. int count; 11. #pragma omp single copyprivate(counter) 12. { 13. counter = 50; 14. } 15. count = increment_counter(); 16. printf("ThreadId: %ld, count = %ld\n", omp_get_thread_num(), count); 17. } 打印结果为: ThreadId: 2, count = 51 ThreadId: 0, count = 51 ThreadId: 3, count = 51 ThreadId: 1, count = 51 如果没有使用 copyprivate 子句,那么打印结果为: ThreadId: 2, count = 1 ThreadId: 1, count = 1 ThreadId: 0, count = 51 ThreadId: 3, count = 1 从打印结果可以看出,使用 copyprivate 子句后,single 构造内给 counter 赋的值被广播到了其他线程里,但没有使用 copyprivate 子句时,只有一个线程获得了 single 构造内的赋值,其他线程没有获取 single 构造内的赋值。 归约操作 reduction 子句主要用来对一个或多个参数条目指定一个操作符,每个线程将创建参数条目的一个私有拷贝,在并行域或任务分担域的结束处,将用私有拷贝的值通过指定的运行符运 算,原始的参数条目被运算结果的值更新。reduction 子 句的格式如下: reduction(operator: list) 下表列出了可以用于 reduction 子句的一些操作符以及对应私有拷贝变量缺省的初始值, 私有拷贝变量的实际初始值依赖于 redtucion 变量的数据类型。 表 2.3 归约操作符与归约变量初值 操作符 初值 +(加) 0 *(减) 1 -(乘) 0 &(按位与) ~0 |(按位或) 0 ^(按位异或) 0 &&(逻辑与) 1 ||(逻辑或) 0 例如一个整数求和的程序如下: 1. int i, sum = 100; 2. #pragma omp parallel for reduction(+: sum) 3. for ( i = 0; i < 1000; i++ ) 4. { 5. sum += i; 6. } 7. printf( "sum = %ld\n", sum); 注意,如果在并行域内不加锁保护就直接对共享变量进行写操作,存在数据竞争问题,会导致不可预测的异常结果。如果共享数据作为 private、firstprivate、 lastprivate、threadprivate、reduction 子句的参数进入并行域后,就变成线程私有了,不需要加锁保护了。 2.3 小结 本章介绍了 OpenMP 的三个基本要素和基本的编程方法。三个要素是编译指导指令、API 函数和环境变量,在编程应用中环境变量只在程序开始运行时做初始设置,而制导指令和 API 可以只运行中发生作用。关于 OpenMP 编程的内容分成 4 个部分,综合应用制导指令、API 函 数和环境变量,分别解决如何控制多个线程的产生、如何利用多个线程对任务进行划分、如何 保证线程间的互斥和同步以及如何控制变量在并行域或任务分担于中的共享属性等等。
/
本文档为【OpenMP 编程基础】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。

历史搜索

    清空历史搜索