OpenMP中的数据处理子句
相关文档连接:
多核编程的几个难题及其应对策略(难题一)
private子句用于将一个或多个变量声明成线程私有的变量,变量声明成私有变量后,
指定每个线程都有它自己的变量私有副本,其他线程无法访问私有副本。即使在并行区域外有同名的共享变量,共享变量在并行区域内不起任何作用,并且并行区域内不会操作到外面的共享变量。
private子句的用法格式如下:
private(list)
下面便是一个使用private子句的代码例子:
int k = 100;
#pragma omp parallel
for private(k)
for ( k=0; k < 10; k++)
{
printf("k=%d/n", k);
}
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子句中。
private声明的私有变量不能继承同名变量的值,但实际情况中有时需要继承原有共享变量的值,OpenMP提供了firstprivate子句来实现这个功能。
先看一下以下的代码例子
int k = 100;
#pragma omp parallel
for firstprivate(k)
for ( i=0; i < 4; i++)
{
k+=i;
printf("k=%d/n",k);
}
printf("last k=%d/n", k);
上面代码执行后打印结果如下:
k=100
k=101
k=103
k=102
last k=100
从打印结果可以看出,并行区域内的私有变量k继承了外面共享变量k的值100作为初始值,并且在退出并行区域后,共享变量k的值保持为100未变。
有时在并行区域内的私有变量的值经过计算后,在退出并行区域时,需要将它的值赋给同名的共享变量,前面的private和firstprivate子句在退出并行区域时都没有将私有变量的最后取值赋给对应的共享变量,lastprivate子句就是用来实现在退出并行区域时将私有变量的值赋给共享变量。
举个例子如下:
int k = 100;
#pragma omp parallel
for firstprivate(k),lastprivate(k)
for ( i=0; i < 4; i++)
{
k+=i;
printf("k=%d/n",k);
}
printf("last k=%d/n", k);
上面代码执行后的打印结果如下:
k=100
k=101
k=103
k=102
last k=103
从打印结果可以看出,退出for循环的并行区域后,共享变量k的值变成了103,而不是保持原来的100不变。
由于在并行区域内是多个线程并行执行的,最后到底是将那个线程的最终计算结果赋给了对应的共享变量呢?OpenMP规范中指出,如果是循环迭代,那么是将最后一次循环迭代中的值赋给对应的共享变量;如果是section构造,那么是最后一个section语句中的值赋给对应的共享变量。注意这里说的最后一个section是指程序语法上的最后一个,而不是实际运行时的最后一个运行完的。
如果是类(class)类型的变量使用在lastprivate参数中,那么使用时有些限制,需要一个可访问的,明确的缺省构造函数,除非变量也被使用作为firstprivate子句的参数;还需要一个拷贝赋值操作符,并且这个拷贝赋值操作符对于不同对象的操作顺序是未指定的,依赖于编译器的定义。
threadprivate子句用来指定全局的对象被各个线程各自复制了一个私有的拷贝,即各个线程具有各自私有的全局对象。
用法如下:
#pragma omp threadprivate(
list
)
new-line
下面用threadprivate命令来实现一个各个线程私有的计数器,各个线程使用同一个函数来实现自己的计数。计数器代码如下:
int counter = 0;
#pragma omp threadprivate(counter)
int increment_counter()
{
counter++;
return(counter);
}
如果对于静态变量也同样可以使用threadprivate声明成线程私有的,上面的counter变量如改成用static类型来实现时,代码如下:
int increment_counter2()
{
static int counter = 0;
#pragma omp threadprivate(counter)
counter++;
return(counter);
}
threadprivate和private的区别在于threadprivate声明的变量通常是全局范围内有效的,而private声明的变量只在它所属的并行构造中有效。
threadprivate的对应只能用于copyin,copyprivate,schedule,num_threads和if子句中,不能用于任何其他子句中。
用作threadprivate的变量的地址不能是常数。
对于C++的类(class)类型变量,用作threadprivate的参数时有些限制,当定义时带有外部初始化时,必须具有明确的拷贝构造函数。
对于windows系统,threadprivate不能用于动态装载(使用LoadLibrary装载)的DLL中,可以用于静态装载的DLL中,关于windows系统中的更多限制,请参阅MSDN中有关threadprivate子句的帮助材料。
有关threadprivate命令的更多限制方面的信息,详情请参阅OpenMP2.5规范。
shared子句用来声明一个或多个变量是共享变量。
用法如下:
shared(
list
)
需要注意的是,在并行区域内使用共享变量时,如果存在写操作,必须对共享变量加以保护,否则不要轻易使用共享变量,尽量将共享变量的访问转化为私有变量的访问。
循环迭代变量在循环构造区域里是私有的。声明在循环构造区域内的自动变量都是私有的。
default子句用来允许用户控制并行区域中变量的共享属性。
用法如下:
default(shared
|
none)
使用shared时,缺省情况下,传入并行区域内的同名变量被当作共享变量来处理,不会产生线程私有副本,除非使用private等子句来指定某些变量为私有的才会产生副本。
如果使用none作为参数,那么线程中用到的变量必须显示指定是共享的还是私有的,除了那些由明确定义的除外。
reduction子句主要用来对一个或多个参数条目指定一个操作符,每个线程将创建参数条目的一个私有拷贝,在区域的结束处,将用私有拷贝的值通过指定的运行符运算,原始的参数条目被运算结果的值更新。
reduction
子句用法如下
:
reduction(
operator
:
list
)
下表列出了可以用于reduction子句的一些操作符以及对应私有拷贝变量缺省的初始值,私有拷贝变量的实际初始值依赖于redtucion变量的数据类型。
表10-4-1:reduction操作中各种操作符号对应拷贝变量的缺省初始值
Operator
|
Initialization value
|
+
|
0
|
*
|
1
|
-
|
0
|
&
|
~0
|
|
|
0
|
^
|
0
|
&&
|
1
|
||
|
0
|
例如一个整数求和的程序如下:
int i, sum = 100;
#pragma omp parallel
for reduction(+: sum)
for ( i = 0; i < 1000; i++ )
{
sum += i;
}
printf(
"sum = %ld/n", sum);
注意,如果在并行区域内不加锁保护就直接对共享变量进行写操作,存在数据竞争问题,会导致不可预测的异常结果。共享数据作为
private
、
firstprivate
、
lastprivate
、
threadprivate
、
reduction
子句的参数进入并行区域后,就变成线程私有了,不需要加锁保护了。
copyin子句用来将主线程中threadprivate变量的值拷贝到执行并行区域的各个线程的threadprivate变量中,便于线程可以访问主线程中的变量值,
用法如下:
copyin(
list
)
copyin中的参数必须被声明成threadprivate的,对于类类型的变量,必须带有明确的拷贝赋值操作符。
对于前面threadprivate中讲过的计数器函数,如果多个线程使用时,各个线程都需要对全局变量counter的副本进行初始化,可以使用copyin子句来实现,示例代码如下:
int main(
int argc, char* argv[])
{
int iterator;
#pragma omp parallel sections
copyin(counter)
{
#pragma omp
section
{
int count1;
for ( iterator = 0; iterator < 100; iterator++ )
{
count1 = increment_counter();
}
printf("count1 = %ld/n", count1);
}
#pragma omp
section
{
int count2;
for ( iterator = 0; iterator < 200; iterator++ )
{
count2 = increment_counter();
}
printf("count2 = %ld/n", count2);
}
}
printf("counter = %ld/n", counter);
}
打印结果如下:
count1 = 100
count2 = 200
counter = 0
从打印结果可以看出,两个线程都正确实现了各自的计数。
copyprivate子句提供了一种机制用一个私有变量将一个值从一个线程广播到执行同一并行区域的其他线程。
用法如下:
copyprivate(
list
)
copyprivate
子句可以关联
single
构造,在
single
构造的
barrier
到达之前就完成了广播工作。
copyprivate
可以对
private
和
threadprivate
子句中的变量进行操作,但是当使用
single
构造时,
copyprivate
的变量不能用于
private
和
firstprivate
子句中。
下面便是一个使用
copyprivate
的代码例子:
int counter = 0;
#pragma omp threadprivate(counter)
int increment_counter()
{
counter++;
return(counter);
}
#pragma omp parallel
{
int count;
#pragma omp single copyprivate(counter)
{
counter = 50;
}
count = increment_counter();
printf("ThreadId: %ld, count = %ld/n", omp_get_thread_num(), count);
}
打印结果为:
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构造内的赋值。
参考文献:
Ananth Grama, Anshul Gupta,“并行计算导论”,张武等译,机械工业出版社,2005.01
Michael J. Quinn, “MPI与OpenMP并行程序设计”,陈文光等译,清华大学出版社,2004.10
Shameem Akhter等,“多核程序设计技术-通过软件多线程提升性能”,电子工业出版社,2007.03
OpenMP2.5规范
http://www.openmp.org/
OpenMP2.0规范
http://www.openmp.org/
MSDN帮助材料
ms-help://MS.VSCC.v80/MS.MSDN.v80/MS.VisualStudio.v80.chs/dv_vclang/html/652414c5-78ed-4b7f-8283-1a9fe4c5e78d.htm
注:本文的写作主要参考OpenMP2.5规范等参考文献,限于水平,可能存在对规范理解不足之处,请大家发现问题时不吝指正。