一、private子句
1. private
private 子句可以将变量声明为线程私有,声明称线程私有变量以后,每个线程都有一个该变量的副本,线程之间不会互相影响,其他线程无法访问其他线程的副本。原变量在并行部分不起任何作用,也不会受到并行部分内部操作的影响。
#include <stdio.h>
#include <omp.h>
int main(int argc, char* argv[])
{
int i = 20;
#pragma omp parallel for private(i)
for (i = 0; i < 10; i++)
{
printf("i = %d\n", i);
}
printf("outside i = %d\n", i);
return 0;
}
//运行结果:
i = 0
i = 2
i = 1
i = 4
i = 3
i = 5
i = 6
i = 7
i = 9
i = 8
outside i = 20
2. firstprivate
private子句不能继承原变量的值,但是有时我们需要线程私有变量继承原来变量的值,这样我们就可以使用firstprivate子句来实现。
int main(int argc, char* argv[])
{
int t = 20, i;
#pragma omp parallel for firstprivate(t)
for (i = 0; i < 5; i++)
{
//次数t被初始化为20
t += i;
printf("t = %d\n", t);
}
//此时t=20
printf("outside t = %d\n", t);
return 0;
}
3. lastprivate
除了在进入并行部分时需要继承原变量的值外,有时我们还需要再退出并行部分时将计算结果赋值回原变量,lastprivate子句就可以实现这个需求。
需要注意的是,根据OpenMP规范,在循环迭代中,是最后一次迭代的值赋值给原变量;如果是section结构,那么是程序语法上的最后一个section语句赋值给原变量。
如果是类(class)变量作为lastprivate的参数时,我们需要一个缺省构造函数,除非该变量也作为firstprivate子句的参数;此外还需要一个拷贝赋值操作符。
int main(int argc, char* argv[])
{
int t = 20, i;
// YOUR CODE HERE
#pragma omp parallel for firstprivate(t), lastprivate(t)
// END OF YOUR CODE
for (i = 0; i < 5; i++)
{
t += i;
printf("t = %d\n", t);
}
printf("outside t = %d\n", t);
return 0;
}
===== OUTPUT =====
t = 20
t = 24
t = 23
t = 21
t = 22
outside t = 24
需要注意的是,lastprivate必须要搭配firstprivate一起使用。
4. threadprivate
threadprivate子句可以将一个变量复制一个私有的拷贝给各个线程,即各个线程具有各自私有的全局对象。
int g = 0;
#pragma omp threadprivate(g)
int main(int argc, char* argv[])
{
int t = 20, i;
#pragma omp parallel
{
g = omp_get_thread_num();
}
#pragma omp parallel
{
printf("thread id: %d g: %d\n", omp_get_thread_num(), g);
}
return 0;
}
这里threadprivate和private有什么区别呢?private是对每一次并行任务都复制一份变量,threadprivate是对每一个线程复制一份变量。当并行任务数量小于线程数的时候,就相当于给每一个线程都复制一个变量,此时private跟threadprivate效果一样,但是当并行任务数量大于线程数的时候,private和threadprivate的效果就不同了。所以线程私有主要是针对一些面向线程的变量,而不是面向任务的变量。
二、shared子句
上面我们介绍了private子句,相对的,就有shread子句。Share子句可以将一个变量声明成共享变量,并且在多个线程内共享。需要注意的是,在并行部分进行写操作时,要求共享变量进行保护,否则不要随便使用共享变量,尽量将共享变量转换为私有变量使用。
int main(int argc, char* argv[])
{
int t = 20, i;
#pragma omp parallel for shared(t)
for (i = 0; i < 10; i++)
{
if (i % 2 == 0)
t++;
printf("i = %d, t = %d\n", i, t);
}
return 0;
}
===== OUTPUT =====
i = 8, t = 21
i = 1, t = 21
i = 0, t = 22
i = 4, t = 23
i = 6, t = 24
i = 3, t = 24
i = 5, t = 24
i = 7, t = 24
i = 9, t = 24
i = 2, t = 25
这里使用对t共享,实际上程序变成了串行。
三、OpenMP中private、share的隐式规则
上面是通过private、shared子句显式地指定数据在线程之间的共享性,当我们不明确指定的时候,规则又是怎么样的呢?
OpenMP有一组规则,可以推论出变量的数据共享属性。
例如,让我们考虑以下代码段:
int i = 0;
int n = 10;
int a = 7;
#pragma omp parallel for
for (i = 0; i < n; i++)
{
int b = a + i;
...
}
有四个变量i
,n
, a
和b
。
通常在并行区域之外声明的变量的数据共享属性。因此,n
和 a
是共享变量。
但是,循环迭代变量默认情况下是私有的。因此, i
是私有的。
在并行区域内本地声明的变量是私有的。因此b
是私有的。
我建议在并行区域内声明循环迭代变量。在这种情况下,很显然,此变量是私有的。上面的代码片段如下所示:
int n = 10; // shared
int a = 7; // shared
#pragma omp parallel for
for (int i = 0; i < n; i++) // i private
{
int b = a + i; // b private
...
}
四、通过default指定变量共享模式
我们可以通过defaut()子句来指定所有变量的共享性,就不用一个一个去打了.
1.default(shared)
default
子句有两个版本。首先,我们关注default(shared)
选项,然后考虑 default(none)
条款。
这两个版本特定于OpenMP的C ++程序员。default
Fortran程序员还有其他可能性。
该default(shared)
子句将构造中所有变量的数据共享属性设置为共享。在下面的例子中
int a, b, c, n;
...
#pragma omp parallel for default(shared)
for (int i = 0; i < n; i++)
{
// using a, b, c
}
a
,b
,c
和 n
被共享变量。
default(shared)
子句的另一种用法是指定大多数变量的数据共享属性,然后另外定义私有变量。这种用法如下所示:
int a, b, c, n;
#pragma omp parallel for default(shared) private(a, b)
for (int i = 0; i < n; i++)
{
// a and b are private variables
// c and n are shared variables
}
2.default(none)
该default(none)
子句强制程序员明确指定所有变量的数据共享属性。
int n = 10;
std::vector<int> vector(n);
int a = 10;
#pragma omp parallel for default(none) shared(n, vector, a)
for (int i = 0; i < n; i++)
{
vector[i] = i * a;
}
3. 两种模式对循环变量和在并行区域内声明的变量的影响
1. 循环变量和在并行区域内声明的变量默认都是私有的
在default(shared)模式下,所有的并行区域外的变量都是shared的模式,对于循环变量和在并行区域内声明的变量,不受该defautl(shared)模式的影响,还是private的,除非你对它们指定为shared模式的;
在default(none)情况下,它们同样不受影响,该模式不会强制你对循环变量和并行区域内变量进行指定,但需要强制你对外部变量进行指定。比如下面,i不会要求强制指定,默认私有,但是不指定a就会报错。
int i = 20; int a = 1;
#pragma omp parallel for default(none) private(a)
for (i = 0; i < 10; i++)
{
printf("i = %d\t%d\n", i, a);
}
printf("outside i = %d\n", i);
return 0;
五、最佳编程实践
程序员最好遵循以下两个准则。
第一条准则是始终使用default(none)
子句编写并行区域 。这迫使程序员明确考虑所有变量的数据共享属性。
第二条准则是在可能的情况下在并行区域内声明私有变量。该准则提高了代码的可读性,并使代码更清晰。
六、参考资料
【1】http://jakascorner.com/blog/2016/06/omp-data-sharing-attributes.html