OpenMP入门
OpenMP是一个业界的标准,很早以前就有了,只是近一段时间才逐渐热起来。我们
可以在C/C++和Fortran使用OpenMP、很容易的引入多线程。
#pragma omp parallel
{
}
这样代码就会并行执行
加上#pragma omp for
for循环中的任务会自动分配到不同线程中
也可以写成#pragma omp parallel for
数据环境
在默认情况下,OpenMP将全局变量、静态变量设置为共享属性。
我们可以通过如下方法来改变OpenMP的变量默认属性,你可以把它设置为共享
(shared)或无。也可以单独改变某几个变量的属性,把他们设置为shared或
private。
#pragma omp parallel for private(x,y)
x,y设置成私有,互不干扰
#pragma omp parallel for share(sum)
sum设置为共享
#include <iostream>
#include <omp.h>
using namespace std;
int main()
{
int sum = 0;
#pragma omp parallel for share(sum)
for(int i = 0;i!=10;++i){
sum += i;
}
cout<< sum;
return 0;
}
输出为45
#pragma omp critical
sum += a[i]+b[i];
可以防止多线程同时对sum进行写操作,
我们可以给临界区命名,在下面例子中,如果我们不给临界区命名,在任一时刻,只
能有一个线程调用consum函数。而我们给临界区命名后,任一时刻可以有最 多2个
线程在调用consum函数(1个调用 consum(B, &R1),另一个调用 consum(A,
&R2)。这在这2句语句可以同时执行的情况下,我们通过临界区命名来尽可能减少线
程等待时间。
归约(Redunction)是个很有用的功能,可以简化我们的编程,op代表一个操作, list是执行这个操作的一个或多个变量。
我 们再看刚才上面的例子就清楚了。我们对sum这个变量使用归约操作,操作符是 +。这样的话,每个线程就会有一个私有的sum变量,当所有线程的计算完成 后,每 个线程的私有的sum的值将被用“+”归约成一个总的sum,即 线程1的sum + 线程2 的sum + ... + 线程n的sum -> 总的sum,这个总的sum值将被带出并行区并赋给 全局的那个sum变量,因此,当这个并行区的代码执行完以后,我们的sum变量的值 就是我们期望得到的 值了。
#pragma omp parallel for reduction(+:sum)
OpenMP中工作量的划分与调度
前面我看到在使用工作量共享(work-sharing)这种方式的时候,工作量是自动给我们 划分好并分配给各个线程的。下面,我们来看看如何来控制工作量的划分与调度。
如上图所示,工作量的划分与调度有3种方式: 1、 静态:把循环的迭代按照每x次(x=chunk)迭代分为一块,这样你的总工作量就 被划分成了n/x块(n为迭代次数、循环次数),然后将这些块按照轮转 法依次分配给各 个线程。举个例子:比如我们有100次迭代,x=chunk=4,那么我们的工作就被分 为25块,假设我们有2个线程可以做工作,那么线程 1分到的块是1,3,5,7....,25, 线程2分到的块是2,4,6,...,24; 2、动态:迭代分块方法同上,但是工作块被放到一个队列中,每个线程每次拿一 块,做好了才能到队列里去拿下一块; 3、Guided:这个方式是动态方式的改进。在这个方式里,分块的x是不固定的,一 开始块的大小(x)比较大,随着剩余工作量的减小,块的大小也随之变小。 我们总结一下每种方式适合什么样的工作量 静态方式:比较适合每次迭代的工作量相近(主要指工作所需时间)的情况 动态方式:比较适合每次迭代的工作量非常不确定的情况 Guided方式:类似动态方式,但是队列相关的开销会比动态方式小
另外几个OpenMP Construct
下面我们来看一看Parallel Section,其实看看下面的图片就知道了,我们可以定义 多个section,让这些section并行的执行。下面的例子是我们有足够的线程来同时执 行这3个section,如果我们只有2个线程,情况会是怎样呢?
如果只有2个线程,那么肯定得有1个线程要比另一个线程勤劳一点,执行2个section 了。 在使用Parallel Section时需要注意的是,每个section的工作之间应该是相互独立、 没有依赖关系的。如果不满足这个要求的话,就不要对他们用并行了。
下面再介绍2个有可能会用到的OpenMP construct。 1、 single:有时候在并行区里,我们希望有部分代码只能执行一次,也就是说只有 一个线程去执行这部分代码。如下面的例 子,ExchangeBoundaries() 这句语句前 面我们加上 #pragma omp single ,就保证只有一个线程去执行它。同时在single 后面会有一个隐含的障碍(implicit barrier)。我们后面会具体介绍障碍这个概念。 #pragma omp parallel { DoManyThings(); #pragma omp single { ExchangeBoundaries(); } // threads wait here for single DoManyMoreThings(); } 2、 master:master跟single很类似。在下面例子中,只有主线程会去执行 ExchangeBoundaries() 这条语句。但是master没有隐含的障碍,因此如果其他线 程遇到 #pragma omp master,就会跳过去,直接执行master后面的语句。 #pragma omp parallel { DoManyThings(); #pragma omp master { // if not master skip to next stmt ExchangeBoundaries(); } DoManyMoreThings(); }
某个线程可能比另外2个线程提前完成工作,但是这个线程不能继
续往下走去执行并行区后面的工作,因为#pragma omp for里面带了隐含的障碍,
这个障碍的意思就是说,所有的线程做完了自己的工作后必须在这里等,直到所有的
线程都完成了各自的工作,大家才能往下走。汇编 里的memory fence与这个有点神
似。
为什么这些construct要带了个隐含的障碍呢?障碍不是让程序执行速度变慢了吗?
因为它怕你程序里面后面的代码对这块代码有依赖关系,如果这块代码的工作没完成
就去执行后面的代码,可能会引起错误。
那如果你后面的代码对这块代码没有依赖,可以用 nowait 来把这个隐含的障碍给去
掉。比如:
例子1:
C/C++ code #pragma omp for nowait for(...) {...};
例子2:
C/C++ code #pragma single nowait{ [...] }
例子3:
C/C++ code
#pragma omp for schedule(dynamic,1) nowait
for(int i=0; i<n; i++)
a = bigFunc1(i);
#pragma omp for schedule(dynamic,1)
for(int j=0; j<m; j++)
b[j] = bigFunc2(j);
介绍几个OpenMP API
在大多数情况下,我们不会用到OpenMP API。一般只有在调试和某些情况下,才需要用到API。
如果你需要使用 OpenMP API,记得先包含OpenMP头文件
C/C++ code #include <omp.h>
最常用的2个API是:
C/C++ code int omp_get_thread_num(void);int omp_get_num_threads(void);
在并行区里调用omp_get_thread_num返回的是当前线程的线程ID,一般是0到(N-1),N是并行区里的总线程数。
在并行区里调用omp_get_num_threads返回的是并行区里的总线程数。
http://blog.csdn.net/gengshenghong/article/details/6985431
OpenMP中数据属性相关子句详解(1):private/firstprivate/lastprivate/threadprivate之间的比较
private/firstprivate/lastprivate/threadprivate,首先要知道的是,它们分为两大类,一类是private/firstprivate/lastprivate子句,另一类是threadprivate,为指令。(PS:有些地方把threadprivate说成是子句,但是实际来讲,它是一个指令。
(1) private
private子句将一个或多个变量声明为线程的私有变量。每个线程都有它自己的变量私有副本,其他线程无法访问。即使在并行区域外有同名的共享变量,共享变量在并行区域内不起任何作用,并且并行区域内不会操作到外面的共享变量。
注意:
1. private variables are undefined on entry and exit of the parallel region.即private变量在进入和退出并行区域是“未定义“的。
2. The value of the original variable (before the parallel region) is undefined after the parallel region!在并行区域之前定义的原来的变量,在并行区域后也是”未定义“的。
3. A private variable within the parallel region has no storage association with the same variable outside of the region. 并行区域内的private变量和并行区域外同名的变量没有存储关联。
说明:private的很容易理解错误。下面用例子来说明上面的注意事项,
A. private变量在进入和退出并行区域是”未定义“的。
- int main(int argc, _TCHAR* argv[])
- {
- int A=100;
- #pragma omp parallel for private(A)
- for(int i = 0; i<10;i++)
- {
- printf("%d\n",A);
- }
- 10.
- 11. return 0;
12. }
初学OpenMP很容易认为这段代码是没有问题的。其实,这里的A在进入并行区域的时候是未定义的,所以在并行区域直接对其进行读操作,会导致运行时错误。
其实,在VS中编译这段代码,就会有编译警告:
warning C4700: uninitialized local variable 'A' used
很清楚的指向"printf"这句,A是没有初始化的变量。所以,运行时候会出现运行时崩溃的错误。
这段代码能说明,private在进入并行区域是未定义的,至于退出并行区域就不容易举例说明了,本身,这里的三个注意事项是交叉理解的,说明的是一个含义,所以,看下面的例子来理解。
B. 在并行区域之前定义的原来的变量,在并行区域后也是”未定义“的。
- int main(int argc, _TCHAR* argv[])
- {
- int B;
- #pragma omp parallel for private(B)
- for(int i = 0; i<10;i++)
- {
- B = 100;
- }
- 10.
- 11. printf("%d\n",B);
- 12.
- 13. return 0;
14. }
这里的B在并行区域内进行了赋值等操作,但是在退出并行区域后,是未定义的。理解”在并行区域之前定义的原来的变量,在并行区域后也是”未定义“的“这句话的时候,要注意,不是说所有的在并行区域内定义的原来的变量,使用了private子句后,退出并行区域后就一定是未定义的,如果原来的变量,本身已经初始化,那么,退出后,不会处于未定义的状态,就是下面的第三个注意事项要说明的问题。
C. 并行区域内的private变量和并行区域外同名的变量没有存储关联
- int main(int argc, _TCHAR* argv[])
- {
- int C = 100;
- #pragma omp parallel for private(C)
- for(int i = 0; i<10;i++)
- {
- C = 200;
- printf("%d\n",C);
- 10. }
- 11.
- 12. printf("%d\n",C);
- 13.
- 14. return 0;
15. }
这里,在退出并行区域后,printf的C的结果是100,和并行区域内对其的操作无关。
总结来说,上面的三点是交叉的,第三点包含了所有的情况。所以,private的关键理解是:A private variable within the parallel region has no storage association with the same variable outside of the region. 简单点理解,可以认为,并行区域内的private变量和并行区域外的变量没有任何关联。如果非要说点关联就是,在使用private的时候,在之前要先定义一下这个变量,但是,到了并行区域后,并行区域的每个线程会产生此变量的副本,而且是没有初始化的。
下面是综合上面的例子,参考注释的解释:
- int main(int argc, _TCHAR* argv[])
- {
- int A=100,B,C=0;
- #pragma omp parallel for private(A) private(B)
- for(int i = 0; i<10;i++)
- {
- B = A + i; // A is undefined! Runtime error!
- printf("%d\n",i);
- 10. }
- 11. /*--End of OpemMP paralle region. --*/
- 12.
- 13. C = B; // B is undefined outside of the parallel region!
- 14. printf("A:%d\n", A);
- 15. printf("B:%d\n", B);
- 16.
- 17. return 0;
18. }
(2)firstprivate
Private子句的私有变量不能继承同名变量的值,firstprivate则用于实现这一功能-继承并行区域额之外的变量的值,用于在进入并行区域之前进行一次初始化。
Firstprivate(list):All variables in the list areinitialized with the value the original object had before entering the parallelconstruct.
分析下面的例子:
- int main(int argc, _TCHAR* argv[])
- {
- int A;
- #pragma omp parallel for firstprivate(A)
- for(int i = 0; i<10;i++)
- {
- printf("%d: %d\n",i, A); // #1
- }
- 10.
- 11. printf("%d\n",A); // #2
- 12.
- 13. return 0;
14. }
用VS编译发现,也会报一个“warning C4700: uninitialized local variable 'A' used”的警告,但是这里其实两个地方用到了A。实际上,这个警告是针对第二处的,可以看出,VS并没有给第一处OpenMP并行区域内的A有警告,这是由于使用firstprivate的时候,会对并行区域内的A使用其外的同名共享变量就行初始化,当然,如果严格分析,外面的变量其实也是没有初始化的,理论上也是可以认为应该报警告,但是,具体而言,这是跟VS的实现有关的,另外,在debug下,上面的程序会崩溃,release下,其实是可以输出值的,总之,上面的输出是无法预料的。
再看下面的例子,和前面private的例子很类似:
- int main(int argc, _TCHAR* argv[])
- {
- int A = 100;
- #pragma omp parallel for firstprivate(A)
- for(int i = 0; i<10;i++)
- {
- printf("%d: %d\n",i, A); // #1
- }
- 10.
- 11. printf("%d\n",A); // #2
- 12.
- 13. return 0;
14. }
这里,如果使用private,那么并行区域内是有问题的,因为并行区域内的A是没有初始化的,导致无法预料的输出或崩溃。但是,使用了firstprivate后,这样,进入并行区域的时候,每一个线程的A的副本都会利用并行区域外的同名共享变量A的值进行一次初始化,所以,输出的A都是100.
继续探讨这里的“进行一次初始化”,为了理解“一次”的含义,看下面的例子:
- #include <omp.h>
- int main(int argc, _TCHAR* argv[])
- {
- int A = 100;
- #pragma omp parallel for firstprivate(A)
- for(int i = 0; i<10;i++)
- {
- printf("Thread ID: %d, %d: %d\n",omp_get_thread_num(), i, A); // #1
- 10. A = i;
- 11. }
- 12.
- 13. printf("%d\n",A); // #2
- 14.
- 15. return 0;
16. }
这里,每次输出后,改变A的值,需要注意的是,这里的“进行一次初始化”是针对team内的每一个线程进行一次初始化,对于上面的程序,在4核的CPU上运行,并行区域内有四个线程,所以每一个线程都会有A的一个副本,因而,上面的程序输出结果可能如下:
其实,这个结果是很容易理解的,不可能是每一个for都有一个变量的副本,而是每一个线程,所以这个结果在预料之中。
仍然借助上面这个例子,帮助理解private和firstprivate,从而引出lastprivate,private对于并行区域的每一个线程都有一个副本,并且和并行区域外的变量没有关联;firstprivate解决了进入并行区的问题,即在进入并行区域的每个线程的副本变量使用并行区域外的共享变量进行一个初始化的工作,那么下面有一个问题就是,如果希望并行区域的副本变量,在退出并行区的时候,能反过来赋值给并行区域外的共享变量,那么就需要依靠lastprivate了。
(3)lastprivate
如果需要在并行区域内的私有变量经过计算后,在退出并行区域时,需要将其值赋给同名的共享变量,就可以使用lastprivate完成。
Lastprivate(list):The thread that executes the sequentially last iteration or section updates thevalue of the objects in the list.
从上面的firstprivate的最后一个例子可以看出,并行区域对A进行了赋值,但是退出并行区域后,其值仍然为原来的值。
这里首先有一个问题是:退出并行区域后,需要将并行区域内的副本的值赋值为同名的共享变量,那么,并行区域内有多个线程,是哪一个线程的副本用于赋值呢?
是否是最后一个运行完毕的线程?否!OpenMP规范中指出,如果是循环迭代,那么是将最后一次循环迭代中的值赋给对应的共享变量;如果是section构造,那么是最后一个section语句中的值赋给对应的共享变量。注意这里说的最后一个section是指程序语法上的最后一个,而不是实际运行时的最后一个运行完的。
在理解这句话之前,先利用一个简单的例子来理解一下lastprivate的作用:
- int main(int argc, _TCHAR* argv[])
- {
- int A = 100;
- #pragma omp parallel for lastprivate(A)
- for(int i = 0; i<10;i++)
- {
- A = 10;
- }
- 10.
- 11. printf("%d\n",A);
- 12.
- 13. return 0;
14. }
这里,很容易知道结果为10,而不是100.这就是lastprivate带来的效果,退出后会有一个赋值的过程。
理解了lastprivate的基本含义,就可以继续来理解上面的红色文字部分的描述了,即到底是哪一个线程的副本用于对并行区域外的变量赋值的问题,下面的例子和前面firstprivate的例子很类似:
- #include <omp.h>
- int main(int argc, _TCHAR* argv[])
- {
- int A = 100;
- #pragma omp parallel for lastprivate(A)
- for(int i = 0; i<10;i++)
- {
- printf("Thread ID: %d, %d\n",omp_get_thread_num(), i); // #1
- 10. A = i;
- 11. }
- 12.
- 13. printf("%d\n",A); // #2
- 14.
- 15. return 0;
16. }
从结果可以看出,最后并行区域外的共享变量的值并不是最后一个线程退出的值,多次运行发现,并行区域的输出结果可能发生变化,但是最终的输出都是9,这就是上面的OpenMP规范说明的问题,退出并行区域的时候,是根据“逻辑上”的最后一个线程用于对共享变量赋值,而不是实际运行的最后一个线程,对于for而言,就是最后一个循环迭代所在线程的副本值,用于对共享变量赋值。
另外,firstprivate和lastprivate分别是利用共享变量对线程副本初始化(进入)以及利用线程副本对共享变量赋值(退出),private是线程副本和共享变量无任何关联,那么如果希望进入的时候初始化并且退出的时候赋值呢?事实上,可以对同一个变量使用firstprivate和lastprivate的,下面的例子即可看出:
- #include <omp.h>
- int main(int argc, _TCHAR* argv[])
- {
- int A = 100;
- #pragma omp parallel for firstprivate(A) lastprivate(A)
- for(int i = 0; i<10;i++)
- {
- printf("Thread ID: %d, %d: %d\n",omp_get_thread_num(), i, A); // #1
- 10. A = i;
- 11. }
- 12.
- 13. printf("%d\n",A); // #2
- 14.
- 15. return 0;
16. }
说明:不能对一个变量同时使用两次private,或者同时使用private和firstprivate/lastprivate,只能firstprivate和lastprivate一起使用。
关于lastprivate,还需要说明的一点是,如果是类(class)类型的变量使用在lastprivate参数中,那么使用时有些限制,需要一个可访问的,明确的缺省构造函数,除非变量也被使用作为firstprivate子句的参数;还需要一个拷贝赋值操作符,并且这个拷贝赋值操作符对于不同对象的操作顺序是未指定的,依赖于编译器的定义。
另外,firstprivate和private可以用于所有的并行构造块,但是lastprivate只能用于for和section组成的并行块之中,参考http://blog.csdn.net/gengshenghong/article/details/6970220的对照表。
(4)threadprivate
首先,threadprivate和上面几个子句的区别在于,threadprivate是指令,不是子句。threadprivate指定全局变量被OpenMP所有的线程各自产生一个私有的拷贝,即各个线程都有自己私有的全局变量。一个很明显的区别在于,threadprivate并不是针对某一个并行区域,而是整个于整个程序,所以,其拷贝的副本变量也是全局的,即在不同的并行区域之间的同一个线程也是共享的。
threadprivate只能用于全局变量或静态变量,这是很容易理解的,根据其功能。
根据下面的例子,来进一步理解threadprivate的使用:
- #include <omp.h>
- int A = 100;
- #pragma omp threadprivate(A)
- int main(int argc, _TCHAR* argv[])
- {
- #pragma omp parallel for
- for(int i = 0; i<10;i++)
- {
- 10. A++;
- 11. printf("Thread ID: %d, %d: %d\n",omp_get_thread_num(), i, A); // #1
- 12. }
- 13.
- 14. printf("Global A: %d\n",A); // #2
- 15.
16. #pragma omp parallel for
- 17. for(int i = 0; i<10;i++)
- 18. {
- 19. A++;
- 20. printf("Thread ID: %d, %d: %d\n",omp_get_thread_num(), i, A); // #1
- 21. }
- 22.
- 23. printf("Global A: %d\n",A); // #2
- 24.
- 25. return 0;
26. }
分析结果,发现,第二个并行区域是在第一个并行区域的基础上继续递增的;每一个线程都有自己的全局私有变量。另外,观察在并行区域外的打印的“Globa A”的值可以看出,这个值总是前面的thread 0的结果,这也是预料之中的,因为退出并行区域后,只有master线程运行。
threadprivate指令也有自己的一些子句,就不在此分析了。另外,如果使用的是C++的类,对于类的构造函数也会有类似于lastprivate的一些限制。
总结:
private/firstprivate/lastprivate都是子句,用于表示并行区域内的变量的数据范围属性。其中,private表示并行区域team内的每一个线程都会产生一个并行区域外同名变量的共享变量,且和共享变量没有任何关联;firstprivaet在private的基础上,在进入并行区域时(或说每个线程创建时,或副本变量构造时),会使用并行区域外的共享变量进行一次初始化工作;lastprivate在private的基础上,在退出并行区域时,会使用并行区域内的副本的变量,对共享变量进行赋值,由于有多个副本,OpenMP规定了如何确定使用哪个副本进行赋值。另外,private不能和firstprivate/lastprivate混用于同一个变量,firstprivate和lastprivate可以对同一变量使用,效果为两者的结合。
threadprivate是指令,和private的区别在于,private是针对并行区域内的变量的,而threadprivate是针对全局的变量的。