OpenMP中数据属性相关子句详解(1):private/firstprivate/lastprivate/threadprivate之间的比较

时间:2022-03-18 04:38:33
private/firstprivate/lastprivate/threadprivate,首先要知道的是,它们分为两大类,一类是private/firstprivate/lastprivate子句,另一类是threadprivate,为指令。(PS:有些地方把threadprivate说成是子句,但是实际来讲,它是一个指令。)
可以参考http://blog.csdn.net/gengshenghong/article/details/6970220可以查看哪些指令能接受哪些子句。


(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);
}

return 0;
}
初学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;
}

printf("%d\n",B);

return 0;
}
这里的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);
}

printf("%d\n",C);

return 0;
}
这里,在退出并行区域后,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);
}
/*--End of OpemMP paralle region. --*/

C = B;// B is undefined outside of the parallel region!
printf("A:%d\n", A);
printf("B:%d\n", B);

return 0;
}

(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
}

printf("%d\n",A);// #2

return 0;
}
用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
}

printf("%d\n",A);// #2

return 0;
}
这里,如果使用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
A = i;
}

printf("%d\n",A);// #2

return 0;
}

这里,每次输出后,改变A的值,需要注意的是,这里的“进行一次初始化”是针对team内的每一个线程进行一次初始化,对于上面的程序,在4核的CPU上运行,并行区域内有四个线程,所以每一个线程都会有A的一个副本,因而,上面的程序输出结果可能如下:

OpenMP中数据属性相关子句详解(1):private/firstprivate/lastprivate/threadprivate之间的比较

其实,这个结果是很容易理解的,不可能是每一个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;
}

printf("%d\n",A);

return 0;
}
这里,很容易知道结果为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
A = i;
}

printf("%d\n",A);// #2

return 0;
}

OpenMP中数据属性相关子句详解(1):private/firstprivate/lastprivate/threadprivate之间的比较

从结果可以看出,最后并行区域外的共享变量的值并不是最后一个线程退出的值,多次运行发现,并行区域的输出结果可能发生变化,但是最终的输出都是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
A = i;
}

printf("%d\n",A);// #2

return 0;
}
说明:不能对一个变量同时使用两次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++)
{
A++;
printf("Thread ID: %d, %d: %d\n",omp_get_thread_num(), i, A);// #1
}

printf("Global A: %d\n",A);// #2

#pragma omp parallel for
for(int i = 0; i<10;i++)
{
A++;
printf("Thread ID: %d, %d: %d\n",omp_get_thread_num(), i, A);// #1
}

printf("Global A: %d\n",A);// #2

return 0;
}

OpenMP中数据属性相关子句详解(1):private/firstprivate/lastprivate/threadprivate之间的比较

分析结果,发现,第二个并行区域是在第一个并行区域的基础上继续递增的;每一个线程都有自己的全局私有变量。另外,观察在并行区域外的打印的“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是针对全局的变量的。