(这篇翻译只涉及与C/C++相关的代码和示例,忽略了与Fortran相关的代码和示例,感兴趣的读者可以参考原文)
OpenMP是由一组计算机硬件和软件供应商联合定义的应用程序接口(API)。OpenMP为基于共享内存的并行程序的开发人员提供了一种便携式和可扩展的编程模型,其API支持各种架构上的C/C++和Fortran。本教程介绍了OpenMP 3.1的大部分主要功能,包括并行区域,工作共享,同步和数据环境的构造和指令。我们同时也包含了对运行时库函数 (Runtime library)和环境变量的介绍。本教程包括C和Fortran的示例代码和实验练习(译注:译者在这里忽略了Fortran语言的示例代码和实验练习)。
先决条件:本教程非常适合那些正在使用OpenMP进行并行编程的人员,需要对C或者Fortran语言中的并行编程有一定了解。对于并行编程的一般概念尚不了解的读者,EC3500: Introduction to Parallel Computing 可能会非常有帮助(译注:译者在此博客上也翻译了这篇帖子,请参见 [并行计算] 1. 并行计算简介)。
OpenMP是:
一组应用程序接口(Application Program Interface, API),可以用来显式地指导多线程、共享内存式程序的并行化。
由如下三个主要API组件构成:1)编译器指令;2)运行时库函数;3)环境变量。
是 Open Multi-Processing 的简称。
OpenMP不是:
OpenMP不是分布式内存并行系统;需要所有供应商一致地实现;保证最有效地利用共享内存;需要程序员去显式地检查数据依赖性、数据冲突、竞争条件、思索,或者导致程序无法保持一致性的代码序列;支持并行I/O操作,但程序员需要负责保证I/O的同步。
OpenMP的目标:
标准化:1)在各种共享内存架构/平台之间提供一套标准;2)由一批主要的计算机硬件和软件供应商联合定义和支持。
精简性:1)位共享内存机器上的编程提供一套简单而有限的指令;2)主要的并行化仅仅通过3-4个指令就可以实现;3)显然,随着新版本的不断发布,该目标变得越来越没有意义。
易用性:1)提供了对串行程序进行增量并行的能力,而不像消息传递库那样需要全有或全无的方法(all or nothing approach);2)提供了实现粗粒度和细粒度并行化的能力。
可移植性:1)该API基于C/C++和Fortran进行定义;2)提供了公开的API和会员论坛;3)已经在大多数计算平台上实现,包括Unix/Linux平台和Windows平台。
历史:
在90年代早期,共享内存机器的供应商提供了类似的,基于指令式的对Fortran语言的扩展:1)用户可以对一段串行的Fortran程序通过指令进行增量修改,指定需要并行化的部分;2)编译器将自动地对这些循环通过跨SMP处理器进行并行化。这种实现在功能上是相似的,但是内部却存在不一致。
第一次标准化的尝试是1994年的ANSI X3H5草稿。它从来未被实施,很大程度上是由于当时大家对分布式内存机器更感兴趣。
但此后不久,更新的共享式内存机器架构开始变得主流。从1997年开始,OpenMP标准开始制定,接过了ANSI X3H5的遗产。由OpenMP架构审查委员会(ARB)领导的原始ARB成员和贡献者如下所示(免责声明:以下合作伙伴名称均来自 OpenMP网站)。
APR成员 | 程序开发者 | 软件供应商 |
---|---|---|
Compaq / Digital | ADINA R&D, Inc. | Absoft Corporation |
Hewlett-Packard Company | ANSYS, Inc. | Edinburgh Portable Compilers |
Intel Corporation | Dash Associates | GENIAS Software GmBH |
International Business Machines (IBM) | Fluent, Inc. | Myrias Computer Technologies, Inc. |
Kuck & Associates, Inc. (KAI) | ILOG CPLEX Division | The Portland Group, Inc. (PGI) |
Silicon Graphics, Inc. | Livermore Software Technology Corporation (LSTC) | |
Sun Microsystems, Inc. | MECALOG SARL | |
U.S. Department of Energy ASCI program | Oxford Molecular Group PLC | |
The Numerical Algorithms Group Ltd.(NAG) |
发布历史:
OpenMP目前仍然在不断的演进中——新的构件和特征在新版本中不断被添加。最开始,C和Fortran的API定义被分开发布,但从2005年开始,它们被一起发布。下面的列表总结了OpenMP的发布历史。
时间 | 版本 |
---|---|
1997年10月 | Fortran 1.0 |
1998年10月 | C/C++ 1.0 |
1999年11月 | Fortran 1.1 |
2000年11月 | Fortran 2.0 |
2002年3月 | C/C++ 2.0 |
2005年5月 | OpenMP 2.5 |
2008年5月 | OpenMP 3.0 |
2011年7月 | OpenMP 3.1 |
2013年7月 | OpenMP 4.0 |
2015年11月 | OpenMP 4.5 |
注意:本指南基于OpenMP 3.1,所有此后新版本的语法和特征并没有包含在此。
参考资料:
共享内存模型: OpenMP专为多处理器/多核机器而设计。其底层架构可以是共享内存的UMA或者NUMA。
基于线程的并行化: 1)OpenMP程序通过使用线程来完成并行化;2)线程是可由操作系统调度的最小处理单元,其特点是可以安排自主运行的子程序;3)线程存在于单个进程的资源中,如果没有这个进程,那么线程也就不存在了;4)通常,线程数与机器处理器/内核的数量相匹配,但是线程的实际使用也取决于具体的应用程序。
显式并行化: 1)OpenMP是一个显式的(而不是自动的)编程模型,为编程者提供了对并行化的完全控制;2)并行化可以简单到仅仅为一段串行程序添加几条编译器指令……3)或者复杂到插入子程序,以建立多层次的并行机制、锁甚至嵌套锁。
分叉-合并模型: 1)OpenMP采用分叉-合并模型(fork-join)实现并行化。2)所有的OpenMP程序都从一个 主线程 开始。主线程串行执行,直到遇到第一个并行区域。3)分叉:之后主线程将创建一组并行线程。4)并行区域内的代码被用大括号包围起来,然后在多个并行线程上被并行执行。5)合并:当并行线程执行完成并行区域内的代码之后,它们进行同步并且自动结束,只剩下主线程。6)并行区域的数量以及并行线程的数量都可以是任意的。
基于编译器指令: 大多数OpenMP的并行化都是通过在C/C++或者Fortran中嵌入编译器指令而实现的。
嵌套并行: 1)API运行在并行区域内再次嵌入并行区域;2)软硬件实现中可能支持,也可能不支持此功能。
动态线程: 1)API也提供了运行时环境,来动态第更改用于执行并行区域的线程数,在有可能的情况下尽可能地有效利用已有资源;2)软硬件实现中可能支持,也可能不支持此功能。
输入输出 (I/O): 1)OpenMP没有对并行I/O做出规定,所以对于多个线程尝试读写同一个文件的情况要特别小心;2)但如果每个线程对不同的文件进行I/O操作,则问题并不重要;3)编程者有完全责任确保I/O在多线程中被正确地执行。
内存模型:频繁更新(flush)? 1)OpenMP在线程内存中提供了一种“松弛一致性”(relaxed-consistency)和“临时视图”(temporary view)。那就是说,线程可以“缓存”它们的数据,而不必要在任何时刻都保持内存数据的实时精确一致性;2)在所有线程都查看共享变量的关键时刻,编程者需要确保所有所有线程都根据需要更新了共享数据。3)更多关于这个……
三类组件: OpenMP API包含三个不同的组件:
由应用程序开发者决定如何使用这些组件。在最简单的情况下,仅仅需要它们中的几个就可以完成并行化。
对所有API组件的实现方式可能会有所不同。例如某种实现可以表示它支持嵌套并行化,但是API却可能让它们只限于主线程。这并不是编程者所期待的?
编译器指令: 编译器指令在你的源代码中可能被显示为注释,并且被编译器所忽略,除非你明显地告诉编译器——通常情况下是设置适当的编译器标识,这将在 5. OpenMP程序编译 中进行讨论。
OpenMP的编译器指令的目标主要有:1)产生一个并行区域;2)划分线程中的代码块;3)在线程之间分配循环迭代;4)序列化代码段;5)同步线程间的工作。
编译器指令的语法如下:
sentinel directive-name [clause, ...]
例如下面的指令,我们将在随后详细介绍这些指令。
#pragma omp parallel default(shared) private(beta,pi)
运行时库函数: OpenMP API中的运行时库函数是不断增长的,其主要目标包括:1)设置和查询线程数;2)查询线程的唯一标识符(ID),线程的祖先标识符,或者线程组的大小等;3)设置和查询动态线程的属性;4)查询是否在并行须臾,以及在什么级别的并行区域中;5)设置和查询嵌套并行;6)设置、初始化以及终止锁或者嵌套锁;7)查询挂钟时间和分辨率。
对C/C++而言,所有的运行时库函数都是子程序。例如:
#include
int omp_get_num_threads(void)
注意对于C/C++,你通常需要包含头文件
,并且是大小写敏感的。运行时库函数将在 7. 运行时库函数 一节中简单讨论,更多的细节可以参考 附录A:运行时库函数。
环境变量: OpenMP提供了一些环境变量,用来在运行时对并行代码的执行进行控制。这些环境变量可以控制:1)设置线程数;2)指定循环如何划分;3)将线程绑定到处理器;4)启用/禁用嵌套并行,设置最大的嵌套并行级别;5)启用/禁用动态线程;6)设置线程堆栈大小;7)设置线程等待策略。
设置OpenMP的环境变量与设置其它环境变量是一样的,它取决于你使用的是什么Shell。例如:
Shell名称 | 设置方法 |
---|---|
csh/tcsh | setenv OMP_NUM_THREADS 8 |
sh/bash | export OMP_NUM_THREADS=8 |
关于OpenMP环境变量的详细讨论可见:8. 环境变量。
OpenMP的一个代码结构示例:
#include <omp.h>
main ()
{
int var1, var2, var3;
Serial code
.
.
.
Beginning of parallel region. Fork a team of threads.
Specify variable scoping
#pragma omp parallel private(var1, var2) shared(var3)
{
Parallel region executed by all threads
.
Other OpenMP directives
.
Run-time Library calls
.
All threads join master thread and disband
}
Resume serial code
.
.
.
}
LC OpenMP实现: 截止2017年6月,LC默认编译器的文档声称对OpenMP的支持如下:
平台 | 编译器 | 版本标识 | 默认版本 | 支持版本 |
---|---|---|---|---|
Linux | Intel C/C++, Fortran | –version | 16.0.3 | OpenMP 4.0 |
GNU C/C++, Fortran | –version | 4.4.7 (TOSS 2) | OpenMP 3.0 | |
4.9.3 (TOSS 3) | OpenMP 4.0 | |||
PGI C/C++, Fortran | -v | 8.0.1 (TOSS 2) | OpenMP 3.0 | |
–version | 16.9-0 (TOSS 3) | OpenMP 3.1 | ||
Clang C/C++ | –version | 3.7.0 (TOSS 2) | OpenMP 3.1 | |
4.0.0 (TOSS 3) | Some OpenMP 4.0 and 4.5 | |||
BG/Q | IBM XL C/C++ | -qversion | 12.1 | OpenMP 3.1 |
IBM XL Fortran | -qversion | 14.1 | OpenMP 3.1 | |
GNU C/C++, Fortran | –version | 4.4.7 | OpenMP 3.0 | |
CORAL EA | IBM XL C/C++ | -qversion | 14.01 beta | OpenMP 4.5 |
IBM XL Fortran | -qversion | 16.01 beta | OpenMP 4.5 | |
GNU C/C++ | –version | 4.9.3 | OpenMP 4.0 | |
GNU Fortran | –version | 4.8.5 | OpenMP 3.1 | |
PGI C/C++, Fortran | -V | 17.4-0 | OpenMP 3.1 | |
–version | 17.4-0 | OpenMP 3.1 | ||
Clang C/C++ | –version | 4.0 beta | OpenMP 4.5 | |
xlflang Fortran | –version | 4.0 beta | OpenMP 4.5 |
为了查看所有的LC编译器版本,请使用下述命令:
TOSS 2, BG/Q:
use -l compilers
TOSS 3, CORAL EA:module avail
可以在这里查看编译器对OpenMP的支持情况:http://openmp.org/wp/openmp-compilers/。
编译: 所有的LC编译器都需要你使用适当的编译器标识来“打开”OpenMP的编译选项。下面的表格给出了每种编译器需要使用的编译器标识。关于MPI的编译器命令可以参见 这里。
编译器平台 | 编译器命令 | OpenMP标识0 |
---|---|---|
Intel | icc | -qopenmp |
Linux | icpc | -openmp |
ifort | ||
GNU | gcc | -fopenmp |
Linux | g++ | |
IBM Blue Gene | g77 | |
CORAL EA | gfortran | |
PGI | pgcc | -mp |
Linux | pgCC | |
CORAL EA | pgf77 | |
pgf90 | ||
Clang | clang | -fopenmp |
Linux | clang++ | |
CORAL EA | ||
xlflang | xlflang | |
CORAL EA | ||
IBM XL | bgxlc_r, bgcc_r | -qsmp=omp |
Blue Gene * | bgxlC_r, bgxlc++_r | |
bgxlc89_r | ||
bgxlc99_r | ||
bgxlf_r | ||
bgxlf90_r | ||
bgxlf95_r | ||
bgxlf2003_r | ||
IBM XL | xlc_r | -qsmp=omp |
CORAL EA * | xlC_r, xlc++_r | |
xlf_r | ||
xlf90_r | ||
xlf95_r | ||
xlf2003_r | ||
xlf2008_r |
* 请确保采用线程安全的编译器——它的名字以 _r 结尾。
编译器文档:
略。
格式:
pragma omp | directive-name | [clause, …] | newline |
---|---|---|---|
所有OpenMP C/C++指令都需要由此开头。 | 一个合法的OpenMP指令。需要出现在pragma之后,所有其它从句之前。 | 可选。从句可以以任意次序出现,并且可以在需要的时候重复出现出现(特殊情况除外)。 | 必须。先于本指令附带的结构化块出现。 |
示例:
#pragma omp parallel default(shared) private(beta,pi)
一般规则:1)大小写敏感;2)OpenMP指令遵循C/C++指令的标准约定;3)每个指令只能指定一个指令名;4)每个指令最多只适用于一个后续的声明,而且该后续声明必须是一个结构化的块;5)在之领航的末尾通过使用反斜杠“\”转移换行符,可以在后续行上“延续”长指令行。
静态(词汇)范围:
孤立指令:
动态范围:
示例:
#pragma omp parallel for
for (int i = 0; i < 10; ++i) {
sub_routine_1();
sub_routine_2();
}
sub_routine_1() {
#pragma omp critical
{
do_something_1();
}
}
sub_routine_2() {
#pragma omp sections
{
do_something_2();
}
}
为什么重要?
目标:并行区域是指一个可以被多个线程执行的代码块。这是OpenMP并行构建的基础。
格式:
#pragma omp parallel [clause ...] newline
if (scalar_expression)
private (list)
shared (list)
default (shared | none)
firstprivate (list)
reduction (operator: list)
copyin (list)
num_threads (integer-expression)
structured_block
注意事项:
多少个线程? 并行区域中的线程数取决于如下因素,其优先级依次为:
omp_set_num_threads()
库函数的使用;线程编号从0(主线程)到 N-1。
动态线程:
omp_get_dynamic()
来确定动态线程是否被启动;omp_set_dynamic()
;2)设置环境变量 OMP_DYNAMIC 为TRUE。嵌套并行区域:
omp_get_nested()
来确定嵌套并行区域是否被启动;omp_set_nested()
;2)设置环境变量OMP_NESTED 为TRUE。从句:
限制: 1)并行区域必须是一个结构化的块,不能跨越多个例程或者代码文件;2)在并行区域内采用分支(goto)是非法的;3)仅仅可以使用一个 IF 从句;4)仅仅允许使用一个NUM_THREADS;5)程序不能依赖于从句出现的次序。
一个并行区域的例子: 一个简单的“Hello World”程序:1)每个线程执行并行区域内的所有代码;2)OpenMP库函数被用来获取线程ID以及总线程数。
#include
main(int argc, char *argv[]) {
int nthreads, tid;
/* Fork a team of threads with each thread having a private tid variable */
#pragma omp parallel private(tid)
{
/* Obtain and print thread id */
tid = omp_get_thread_num();
printf("Hello World from thread = %d\n", tid);
/* Only master thread does this */
if (tid == 0) {
nthreads = omp_get_num_threads();
printf("Number of threads = %d\n", nthreads);
}
} /* All threads join master thread and terminate */
}
总览:
点击 GO TO THE EXERCISE HERE 开始,大约需要20分钟。
工作共享构造将一段封闭代码划分给当前组内的线程;工作共享构造并不会启动新的线程;在进入工作共享构造之处没有隐含的屏障,但是在工作共享手造结束之处存在隐含的障碍。
工作共享构造的分类:
Do/For | SECTIONS | SINGLE |
---|---|---|
在组线程成员之间分享循环迭代。这种构造代表一种“数据并行” | 将工作分解为独立的部分,每个部分由一个线程执行,代表一种“功能并行” | 将一段代码串行执行。该部分的所有代码都由一个线程独立完成 |
限制条件: 1)工作共享结构必须在并行区域内动态封闭,以便指令并行执行;2)工作共享结构必须由组内成员全部遇到或者全部都没遇到;3)一个组的所有成员必须以相同的顺序遇到连续的工作共享结构。
目标: DO/For指令指定紧随其后的循环迭代必须由组内线程并行执行。这里假设并行区域已经启动,否则这些循环迭代将只会在单个处理器上串行执行。
形式:
#pragma omp for [clause ...] newline
schedule (type [,chunk])
ordered
private (list)
firstprivate (list)
lastprivate (list)
shared (list)
reduction (operator: list)
collapse (n)
nowait
for_loop
从句:
chunk
的一系列块,然后这些块被静态地分配给不同的线程。如果chunk
没有被明确定义,则迭代被平均(如果可能)并且连续第分配给组内线程。chunk
的一系列块,并在线程之间动态调度;当一个线程完成一个块时,它被动态地分配给另外一个块。默认的chunk
大小为1。number_of_iterations / number_of_threads
成正比;随后的块与number_of_iterations_remaining / number_of_threads
成正比;参数chunk
定义最小块大小,默认大小为1。OMP_SCHEDULE
推迟到运行时为止。为这一类型的调度策略指定chunk
大小是非法的。关于其余从句的详细描述请参见:6.12 数据范围/属性从句。
限制条件:
示例:
一个简单的向量加程序:1)数组A, B, C以及变量N将被所有线程共享;2)变量I将成为不同线程内的私有变量,每个线程将会拥有其唯一的副本;3)循环迭代将会被动态地在线程之间分配,其块大小为CHUNK
;4)线程在完成它们各自的块之后将不会同步(NOWAIT)。
#include
#define N 1000
#define CHUNKSIZE 100
main(int argc, char *argv[]) {
int i, chunk;
float a[N], b[N], c[N];
/* Some initializations */
for (i=0; i < N; i++)
a[i] = b[i] = i * 1.0;
chunk = CHUNKSIZE;
#pragma omp parallel shared(a,b,c,chunk) private(i)
{
#pragma omp for schedule(dynamic,chunk) nowait
for (i=0; i < N; i++)
c[i] = a[i] + b[i];
} /* end of parallel region */
}
目标: SECTIONS指令是一个非迭代的工作共享结构,它表明封闭的代码段将在组内线程之间划分。独立的SECTION指令被嵌套在SECTIONS指令内。每个SECTION由组内的一个线程执行一次,不同的SECTION部分可能会由不同的线程来执行。如果某个线程执行的足够快并且实现中也允许这样,那么一个线程也有可能在实际中执行多个SECTION部分。
格式:
#pragma omp sections [clause ...] newline
private (list)
firstprivate (list)
lastprivate (list)
reduction (operator: list)
nowait
{
#pragma omp section newline
structured_block
#pragma omp section newline
structured_block
}
从句:
NOWAIT/nowait
被明确指定。问题:
限制:
示例:
这里给出一个简单的程序,用以说明不同的线程将会执行不同块中的工作。
#include
#define N 1000
main(int argc, char *argv[]) {
int i;
float a[N], b[N], c[N], d[N];
/* Some initializations */
for (i=0; i < N; i++) {
a[i] = i * 1.5;
b[i] = i + 22.35;
}
#pragma omp parallel shared(a,b,c,d) private(i)
{
#pragma omp sections nowait
{
#pragma omp section
for (i=0; i < N; i++)
c[i] = a[i] + b[i];
#pragma omp section
for (i=0; i < N; i++)
d[i] = a[i] * b[i];
} /* end of sections */
} /* end of parallel region */
}
目标: SINGLE指令指定所附代码仅由组内的一个线程来执行。这在处理非线程安全的代码部分(如I/O时)可能会很有用。
格式:
#pragma omp single [clause ...] newline
private (list)
firstprivate (list)
nowait
structured_block
从句:
NOWAIT/nowait
被明确指定。限制:
OpenMP提供了三个只是提供便利的指令:1)PARALLEL DO / parallel for;2)PARALLEL SECTIONS;3)PARALLEL WORKSHARE (仅限于FORTRAN)。
在大多数情况下,这些指令与后面紧随着单独工作共享指令的单独PARALLEL的行为相同。
大多数适用于这两条指令的规则,从句或者限制都有效,更多详细信息可以参考OpenMP API。
使用PARALLEL DO/parallel组合指令的示例如下所示。
#include
#define N 1000
#define CHUNKSIZE 100
main(int argc, char *argv[]) {
int i, chunk;
float a[N], b[N], c[N];
/* Some initializations */
for (i=0; i < N; i++)
a[i] = b[i] = i * 1.0;
chunk = CHUNKSIZE;
#pragma omp parallel for shared(a,b,c,chunk) private(i) schedule(static,chunk)
for (i=0; i < n; i++)
c[i] = a[i] + b[i];
}
目标: 1)TASK指令定义了一个显式任务,该任务可以由遇到的线程执行,或者由组内的任务其它线程延迟执行。2)任务的数据环境由数据共享属性从句确定。3)任务的执行需要进行任务调度——有关详细信息可以参考 OpenMP 3.1文档说明。4)另请参阅OpenMP 3.1文档中的taskyield指令和taskwait指令。
格式:
#pragma omp task [clause ...] newline
if (scalar expression)
final (scalar expression)
untied
default (shared | none)
mergeable
private (list)
firstprivate (list)
shared (list)
structured_block
从句和限制:
总览:
点击 GO TO THE EXERCISE HERE 开始,大约需要20分钟。
思考如下一个简单示例:两个线程或者两个不同的处理器同时试图去对变量x进行自增操作(x的初始值为0)。
线程1:
increment(x) {
x = x + 1;
}
THREAD 1:
10 LOAD A, (x address)
20 ADD A, 1
30 STORE A, (x address)
线程2:
increment(x)
{
x = x + 1;
}
THREAD 2:
10 LOAD A, (x address)
20 ADD A, 1
30 STORE A, (x address)
一种可能的执行序列是:1)线程1在寄存器A上加载x的值;2)线程1在寄存器上加1;3)线程2在寄存器A上加载x的值;4)线程2在寄存器上加1;5)线程1将寄存器A上的值存储回x;6)线程2将寄存器A上的值存储回x。
此时x的值为1,而不是2。为了避免类似情况的发生,对x的自增运算必须在线程之间被同步,以保证运算结果的正确性。
OpenMP提供了多种同步机制,以控制每个线程相对于其它线程如何执行。
目标:
格式:
#pragma omp master newline
structured_block
限制:
目标:
格式:
#pragma omp critical [ name ] newline
structured_block
注意事项:
限制:
示例: 组内所有的线程都试图去并行执行。但是由于CRITICAL区块的存在,任何时刻最多只能有一个线程去执行自增操作。
#include
main(int argc, char *argv[]) {
int x = 0;
#pragma omp parallel shared(x)
{
#pragma omp critical
x = x + 1;
} /* end of parallel region */
}
目标:
格式:
#pragma omp barrier newline
限制:
目标:
格式:
#pragma omp taskwait newline
限制:
目标:
格式:
#pragma omp atomic newline
statement_expression
限制:
目标:
有关更详细信息,请参阅最新的OpenMP规范。
格式:
#pragma omp flush (list) newline
注意事项:
barrier
parallel - upon entry and exit
critical - upon entry and exit
ordered - upon entry and exit
for - upon exit
sections - upon exit
single - upon exit
目标:
格式:
#pragma omp for ordered [clauses...]
(loop region)
#pragma omp ordered newline
structured_block
(endo of loop region)
限制:
目标: THREADPRIVATE指令用于在执行并行区域时,将全局变量(C/C++)变为线程的本地变量。
格式:
#pragma omp threadprivate (list)
注意事项: 该指令必须在声明列出的变量/公共块之后出现。然后每个线程都将获得自己的变量/公共块的副本,所以一个线程写入的数据对于其它编程而言是不可见的。例如:
#include
int a, b, i, tid;
float x;
#pragma omp threadprivate(a, x)
main(int argc, char *argv[]) {
/* Explicitly turn off dynamic threads */
omp_set_dynamic(0);
printf("1st Parallel Region:\n");
#pragma omp parallel private(b,tid)
{
tid = omp_get_thread_num();
a = tid;
b = tid;
x = 1.1 * tid +1.0;
printf("Thread %d: a,b,x= %d %d %f\n",tid,a,b,x);
} /* end of parallel region */
printf("************************************\n");
printf("Master thread doing serial work here\n");
printf("************************************\n");
printf("2nd Parallel Region:\n");
#pragma omp parallel private(tid)
{
tid = omp_get_thread_num();
printf("Thread %d: a,b,x= %d %d %f\n",tid,a,b,x);
} /* end of parallel region */
}
Output:
1st Parallel Region:
Thread 0: a,b,x= 0 0 1.000000
Thread 2: a,b,x= 2 2 3.200000
Thread 3: a,b,x= 3 3 4.300000
Thread 1: a,b,x= 1 1 2.100000
************************************
Master thread doing serial work here
************************************
2nd Parallel Region:
Thread 0: a,b,x= 0 0 1.000000
Thread 3: a,b,x= 3 0 4.300000
Thread 1: a,b,x= 1 0 2.100000
Thread 2: a,b,x= 2 0 3.200000
在首次进入并行区域时,除非在PARALLEL指令中制定了COPYIN从句,否则THREADPRIVATE变量和公共块中的数据应该被视为未定义。
THREADPRIVATE变量与PRIVATE变量(稍后将讨论)不同,因为它们能够在代码的不同并行区域之间持续存在。
限制:
也被成为数据共享属性从句。对数据范围的理解和使用是OpenMP编程的一个重要考虑因素。由于OpenMP是基于共享内存编程模型的,所以大多数变量在默认情况下都是共享的。
全局变量包括:
私有变量包括:
OpenMP数据范围属性从句用来显式定义各个变量的有效范围,它们包括:
数据范围属性从句和一些指令(PARALLEL, DO/for以及SECTIONS)等被一起使用,以控制封闭区域内的变量的有效范围。
这些构造提供了并行构造运行时控制数据环境的能力:
数据范围属性从句仅仅在其词汇/静态范围内有效。
重要提示:有关次主题的重要细节和讨论,请参阅最新的OpenMP规范。
位便于查阅,我们提供了一张表格6.13 从句/指令总结。
目标:
格式:
private (list)
注意事项:
类型 | PRIVATE | THREADPRIVATE |
---|---|---|
数据项 | 变量 | 变量 |
声明处 | 共享任务区域的开始处 | 每个采用块的例程处或者全局文件处 |
一致性 | 不保证一致性 | 保证一致性 |
扩展 | 仅限于词汇-除非作为子程序的参数传递 | 动态 |
初始化 | 采用FIRSTPRIVATE | 采用COPYIN |
目标:
格式:
shared (list)
注意事项:
目标:
格式:
default (shared | none)
注意事项:
限制:
目标:
格式:
firstprivate (list)
注意事项:
目标:
- LASTPRIVATE从句包含了PRIVATE从句的功能以及从最后一个循环或者section中向原始对象变量赋值的功能。
格式:
lastprivate (list)
注意事项:
目标:
格式:
copyin (list)
注意事项:
目标:
格式:
copyprivate (list)
目标:
格式:
reduction (operator: list)
操作 | 操作符 | 初始值 |
---|---|---|
加法 | + | 0 |
乘法 | * | 1 |
减法 | - | 0 |
逻辑与 | && | 0 |
逻辑或 | || | 0 |
按位与 | & | 1 |
按位或 | | | 0 |
按位异或 | ^ | 0 |
相等 | true | |
不等 | false | |
最大值 | max | 最小负值 |
最小值 | min | 最大正值 |
REDUCTION实例:向量点乘
#include
main(int argc, char *argv[]) {
int i, n, chunk;
float a[100], b[100], result;
/* Some initializations */
n = 100;
chunk = 10;
result = 0.0;
for (i=0; i < n; i++) {
a[i] = i * 1.0;
b[i] = i * 2.0;
}
#pragma omp parallel for default(shared) private(i) \
schedule(static,chunk) reduction(+:result)
for (i=0; i < n; i++) {
result = result + (a[i] * b[i]);
}
printf("Final result= %f\n",result);
}
限制条件:
从句 | PARALLEL | DO/For | SECTIONS | SINGLE | PARALLEL DO/For | PARALLEL SECTIONS |
---|---|---|---|---|---|---|
IF | yes | yes | yes | |||
PRIVATE | yes | yes | yes | yes | yes | yes |
SHARED | yes | yes | yes | yes | ||
DEFAULT | yes | yes | yes | |||
FIRSTPRIVATE | yes | yes | yes | yes | yes | yes |
LASTPRIVATE | yes | yes | yes | yes | ||
REDUCTION | yes | yes | yes | yes | yes | |
COPYIN | yes | yes | yes | |||
COPYPRIVATE | yes | |||||
SCHEDULE | yes | yes | ||||
ORDERED | yes | yes | ||||
NOWAIT | yes | yes | yes |
以下OpenMP指令不接受从句:
在不同实现中指令支持的从句可能会有所不同。
本节可主要作为管理OpenMP指令和绑定规则的快速参考。要了解其它规则,用户可参考其实现文档以及OpenMP标准。
指令绑定:
指令嵌套:
OpenMP API的运行时库函数仍然在不断增长中。这些运行时库函数的目标各异,如下表所示:
库函数 | 目标 |
---|---|
OMP_SET_NUM_THREADS | 设置在下一个并行区域中使用的线程数 |
OMP_GET_NUM_THREADS | 返回当前处于执行调用的并行区域中的线程数 |
OMP_GET_MAX_THREADS | 返回调用OMP_GET_NUM_THREADS函数可以返回的最大值 |
OMP_GET_THREAD_NUM | 返回组内线程的线程号(译注:不要和线程总数搞混) |
OMP_GET_THREAD_LIMIT | 返回可用于程序的最大OpenMP线程数 |
OMP_GET_NUM_PROCS | 返回程序可用的处理器数 |
OMP_IN_PARALLEL | 用于确定正在执行的代码是否是并行的 |
OMP_SET_DYNAMIC | 启动或者禁用可执行并行区域的线程数(由运行时系统)的动态调整 |
OMP_GET_DYNAMIC | 用于确定是否启动了动态线程调整 |
OMP_SET_NESTED | 用于启用或者禁用嵌套并行 |
OMP_GET_NESTED | 用于确定嵌套并行是否被弃用 |
OMP_SET_SCHEDULE | 当“运行时”被用作OpenMP指令中的调度类型时,设置循环调度策略 |
OMP_GET_SCHEDULE | 当“运行时”被用作OpenMP指令中的调度类型时,返回循环调度策略 |
OMP_SET_MAX_ACTIVE_LEVELS | 设置嵌套并行区域的最大数量 |
OMP_GET_MAX_ACTIVE_LEVELS | 返回嵌套并行区域的最大数量 |
OMP_GET_LEVEL | 返回嵌套并行区域的当前级别 |
OMP_GET_ANCESTOR_THREAD_NUM | 给定当前线程的嵌套级别,返回其祖先线程的线程号 |
OMP_GET_TEAM_SIZE | 给定当前线程的嵌套级别,返回其线程组的大小 |
OMP_GET_ACTIVE_LEVEL | 返回包含调用任务的的嵌套活动并行区域的数量 |
OMP_IN_FINAL | 如果在最终任务区域中执行该例程,则返回true;否则返回false |
OMP_INIT_LOCK | 初始化与锁变量相关联的锁 |
OMP_DESTROY_LOCK | 解除给定的锁变量与所有锁的关联 |
OMP_SET_LOCK | 获取锁的所有权 |
OMP_UNSET_LOCK | 释放锁 |
OMP_TEST_LOCK | 尝试设置锁,但是如果锁不可用,则不会阻止 |
OMP_INIT_NEST_LOCK | 初始化与锁定变量关联的嵌套锁 |
OMP_DESTROY_NEST_LOCK | 将给定的嵌套锁变量与所有锁解除关联 |
OMP_SET_NEST_LOCK | 获取嵌套锁的所有权 |
OMP_UNSET_NEST_LOCK | 释放嵌套锁 |
OMP_TEST_NEST_LOCK | 尝试设置嵌套锁,但如果锁不可用,则不会阻止 |
OMP_GET_WTIME | 提供便携式挂钟计时程序 |
OMP_GET_WTICK | 返回连续时钟之间的秒数(双精度浮点值) |
对于C/C++而言,所有的运行时坤函数相当于子程序,例如下面的代码:
#include
int omp_get_num_threads(void)
对于C/C++而言,你通常需要包含
头文件。
对于锁运行时/函数而言:
omp_lock_t
或者omp_nest_lock_t
。实现注意事项:
关于运行时函数的更详细讨论请见:附录A:运行时库函数。
OpenMP提供了如下环境变量,以用于对并行代码的控制。所有环境变量的名称都是大写的,而赋予它们的值则是大小写不敏感的。
OMP_SCHEDULE:仅仅适用于for, parallel for指令在调度从句被设置为RUNTIME的情况。该变量的值确定了处理器中的循环迭代如何被调度。例如:
setenv OMP_SCHEDULE "guided, 4"
setenv OMP_SCHEDULE "dynamic"
OMP_NUM_THREADS:设置在运行中可用的最大线程数,例如:
setenv OMP_NUM_THREADS 8
OMP_DYNAMIC:启用或者禁用在执行并行区域时可用线程数的动态调整。其合法的值为TRUE或者FALSE。例如:
setenv OMP_DYNAMIC TRUE
OMP_PROC_BIND:启用或者禁用与处理器绑定的线程,有效值为TRUE或者FALSE。例如:
setenv OMP_PROC_BIND TRUE
OMP_NESTED:启用或者禁用嵌套并行,其有效值为TRUE或者FALSE。例如:
setenv OMP_NESTED TRUE
OMP_STACKSIZE:用于控制所创建的线程(非主线程)的栈空间大小。例如:
setenv OMP_STACKSIZE 2000500B
setenv OMP_STACKSIZE "3000 k "
setenv OMP_STACKSIZE 10M
setenv OMP_STACKSIZE " 10 M "
setenv OMP_STACKSIZE "20 m "
setenv OMP_STACKSIZE " 1G"
setenv OMP_STACKSIZE 20000
OMP_WAIT_POLICY:为OpenMP的实现提供一种关于等待线程所需行为的提示。一个兼容的OpenMP实现可能会也可能不会遵循环境变量的设置。其有效值为ACTIVE或者PASSIVE。ACTIVE指定等待线程主要是活动的,即在等待时消耗处理器周期;PASSIVE指定等待线程主要是被动的,即等待时不消耗处理器周期。ACTIVE和PASSIVE行为的细节是实现定义的。例如:
setenv OMP_WAIT_POLICY ACTIVE
setenv OMP_WAIT_POLICY active
setenv OMP_WAIT_POLICY PASSIVE
setenv OMP_WAIT_POLICY passive
OMP_MAX_ACTIVE_LEVELS:控制活动嵌套并行区域的最大数量。此环境变量的值必须为非负整数。如果OMP_MAX_ACTIVE_LEVELS的请求值大于实现可支持的活动嵌套并行级别的最大数量,后者该值不是非负整数,则程序的行为是实现定义的。例如:
setenv OMP_MAX_ACTIVE_LEVELS 2
OMP_THREAD_LIMIT:设置用于整个OpenMP程序的线程数。此环境变量的值必须为正整数。如果OMP_THREAD_LIMIT的请求值大于实现可以支持的线程数,或者该值不是正整数,则程序的行为是实现定义的。例如:
setenv OMP_THREAD_LIMIT 8
线程栈大小:
编译器 | 栈大小估计 | 数组大小估计(双精度浮点数) |
---|---|---|
Linux icc, ifort | 4 MB | 700 x 700 |
Linux pgcc, pgf90 | 8 MB | 1000 x 1000 |
Linux gcc, gfortran | 2 MB | 500 x 500 |
- 超过其堆栈分配的线程有可能会也有可能不会发生故障。当数据被破坏时,应用程序也有可能会继续运行。
- 静态链接代码可能会受到进一步的堆栈限制。
- 用户的登录shell也有可能会限制堆栈大小。
- 如果你的OpenMP环境支持OpenMP 3.0 OMP_STACKSIZE环境变量(上一节所述),则可以在程序执行之前使用它来设置线程堆栈大小。例如:
setenv OMP_STACKSIZE 2000500B
setenv OMP_STACKSIZE "3000 k "
setenv OMP_STACKSIZE 10M
setenv OMP_STACKSIZE " 10 M "
setenv OMP_STACKSIZE "20 m "
setenv OMP_STACKSIZE " 1G"
setenv OMP_STACKSIZE 20000
// csh/tcsh:
setenv KMP_STACKSIZE 12000000
limit stacksize unlimited
// ksh/sh/bash
export KMP_STACKSIZE = 12000000
ulimit -s unlimited
线程绑定:
setenv OMP_PROC_BIND TRUE
setenv OMP_PROC_BIND FALSE
线程的监听和调试:
调试器处理线程的能力各不相同。TotalView挑食其实LC推荐的并行程序调试器,它非常适合监控和调试多线程程序。
使用OpenMP代码的TotalView会话的示例屏幕截图如下所示:
更详细的信息可以参见:TotalView Debugger tutorial。
Linux ps命令提供了几个用于查看线程信息的标志,一些例子如下所示。有关更详细信息,请参见:Linux User’s Manual。
% ps -Lf
UID PID PPID LWP C NLWP STIME TTY TIME CMD
blaise 22529 28240 22529 0 5 11:31 pts/53 00:00:00 a.out
blaise 22529 28240 22530 99 5 11:31 pts/53 00:01:24 a.out
blaise 22529 28240 22531 99 5 11:31 pts/53 00:01:24 a.out
blaise 22529 28240 22532 99 5 11:31 pts/53 00:01:24 a.out
blaise 22529 28240 22533 99 5 11:31 pts/53 00:01:24 a.out
% ps -T
PID SPID TTY TIME CMD
22529 22529 pts/53 00:00:00 a.out
22529 22530 pts/53 00:01:49 a.out
22529 22531 pts/53 00:01:49 a.out
22529 22532 pts/53 00:01:49 a.out
22529 22533 pts/53 00:01:49 a.out
% ps -Lm
PID LWP TTY TIME CMD
22529 - pts/53 00:18:56 a.out
- 22529 - 00:00:00 -
- 22530 - 00:04:44 -
- 22531 - 00:04:44 -
- 22532 - 00:04:44 -
- 22533 - 00:04:44 -
LC的Linux集群还提供了监视结点上的进程的最高级命令。如果与-H标志一起使用,则进程中包含的线程将可见。top -H命令的示例如下所示。父进程是PID 18010,它产生三个线程,如PID 18012, 18013和18014所示。
性能分析工具:
总览:
点击 GO TO THE EXERCISE HERE 开始,大约需要20分钟。
OMP_SET_NUM_THREADS
目标:
格式:
#include
void omp_set_num_threads(int num_threads)
注意事项:
OMP_GET_NUM_THREADS
目标:
格式:
#include
int omp_get_num_threads(void)
注意事项及限制条件:
OMP_GET_MAX_THREADS
目标:
#include
int omp_get_max_threads(void)
注意事项及限制条件:
OMP_GET_THREAD_NUM
目标:
格式:
#include
int omp_get_thread_num(void)
注意事项及限制条件:
OMP_GET_THREAD_LIMIT
目标:
格式:
#include
int omp_get_thread_limit (void)
注意事项:
OMP_GET_NUM_PROCS
目标:
格式:
#include
int omp_get_num_procs(void)
OMP_IN_PARALLEL
目标:
格式:
#include
int omp_in_parallel(void)
注意事项及限制条件:
OMP_SET_DYNAMIC
目标:
格式:
#include
void omp_set_dynamic(int dynamic_threads)
注意事项及限制条件:
OMP_GET_DYNAMIC
目标:
格式:
#include
int omp_get_dynamic(void)
注意事项及限制条件:
OMP_SET_NESTED
目标:
格式:
#include
void omp_set_nested(int nested)
注意事项及限制条件:
OMP_GET_NESTED
目标:
格式:
#include
int omp_get_nested (void)
注意事项及限制条件:
OMP_SET_SCHEDULE
目标:
格式:
#include
void omp_set_schedule(omp_sched_t kind, int modifier)
OMP_GET_SCHEDULE
目标:
格式:
#include
void omp_get_schedule(omp_sched_t * kind, int * modifier )
OMP_SET_MAX_ACTIVE_LEVELS
目标:
格式:
#include
void omp_set_max_active_levels (int max_levels)
注意事项及限制条件:
OMP_GET_MAX_ACTIVE_LEVELS
目标:
格式:
#include
int omp_get_max_active_levels(void)
OMP_GET_LEVEL
目标:
格式:
#include
int omp_get_level(void)
注意事项及限制条件:
OMP_GET_ANCESTOR_THREAD_NUM
目标:
格式:
#include
int omp_get_ancestor_thread_num(int level)
注意事项和限制条件:
OMP_GET_TEAM_SIZE
目标:
格式:
#include
int omp_get_team_size(int level);
注意事项和限制条件:
OMP_GET_ACTIVE_LEVEL
目标:
格式:
#include
int omp_get_active_level(void);
注意事项和限制条件:
OMP_IN_FINAL
目标:
格式:
#include
int omp_in_final(void)
OMP_INIT_LOCK
OMP_INIT_NEST_LOCK
目标:
格式:
#include
void omp_init_lock(omp_lock_t *lock)
void omp_init_nest_lock(omp_nest_lock_t *lock)
注意事项及限制条件:
OMP_DESTROY_LOCK
OMP_DESTROY_NEST_LOCK
目标:
格式:
#include
void omp_destroy_lock(omp_lock_t *lock)
void omp_destroy_nest_lock(omp_nest_lock_t *lock)
注意事项及限制条件:
OMP_SET_LOCK
OMP_SET_NEST_LOCK
目标:
格式:
#include
void omp_set_lock(omp_lock_t *lock)
void omp_set_nest__lock(omp_nest_lock_t *lock)
注意事项和限制条件:
OMP_UNSET_LOCK
OMP_UNSET_NEST_LOCK
目标:
格式:
#include
void omp_unset_lock(omp_lock_t *lock)
void omp_unset_nest__lock(omp_nest_lock_t *lock)
注意事项和限制条件:
OMP_TEST_LOCK
OMP_TEST_NEST_LOCK
目标:
格式:
#include
int omp_test_lock(omp_lock_t *lock)
int omp_test_nest__lock(omp_nest_lock_t *lock)
注意事项和限制条件:
OMP_GET_WTIME
目标:
格式:
#include
double omp_get_wtime(void)
OMP_GET_WTICK
目标:
格式:
#include
double omp_get_wtick(void)