OpenMP中的数据共享规则(data sharing rules)

Data-Sharing Rules

变量在OpenMP中可以是共享(shared)还是私有(private)的,共享和私有被称作变量Data-sharing的属性(attribute)。如果变量是共享的,在所有线程中存在一个实例使得这个变量被共享。如果变量是私有的,每个线程将会得到私有变量的local copy。

Implicit Rules

OpenMP有一系列规则,来推测数据共享的属性(attributes)。

例如,考虑下面这段代码:

int i = 0; 
int n = 10; 
int a = 7;

​#pragma omp parallel for 
for (i = 0; i < n; i++) {
    int b = a + i;   
    ... 
}

有4个变量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
    ...
}

Explicit Rules

我们可以显式设置变量Data-sharing的属性。

Shared

shared(list)子句声明在list中的变量都是共享的,例子:

#pragma omp parallel for shared(n, a)
for (int i = 0; i < n; i++)
{
    int b = a + i;
    ...        
}

an都是共享变量。

注意OpenMP没有给出一个机制来防止共享变量之间的data race。这应该是程序员的责任。

所谓data race就是不同线程同时读入一个变量,不同时写一个变量,会造成结果不对。

例如:

做求和,四个线程需要将求出的结果放在变量sum里。那么这四个线程如果同时读写sum可能会出错(四个线程同时读到一个老值,得到的新值将仅仅是某一个线程的结果加到sum里)。具体来说: 设sum=6,线程1得到1,线程2得到2,线程3得到3,线程4得到4, 四个线程现在想要将算出来的结果同时写到sum里, 假设四个线程运行的求和代码为sum = sum + a;其中a为每个线程求得的值 此时等式右边的sum为6,所以对于线程1来说,现在要运行的就是sum = 6 + 1,写入得新sum=7, 但是线程2在线程1写入前,就已经读到sum=6,那么对于线程2,运行的就是sum = 6 + 2,得到sum=8. 其他两个依次类推, 所以最终得到的值是什么不确定。

共享变量会引入额外开销,因为变量的一个实例在多个线程之间共享。当需要良好性能时候,应该尽量减少共享变量的数量。

Private

private(list)子句声明在list中的变量都是私有的

#pragma omp parallel for shared(n, a) private(b)
for (int i = 0; i < n; i++)
{
    b = a + i;

    ...
}

这里变量b是私有变量。每个线程都有变量b的local copy。

私有变量有时候是反直觉的。假设私有变量在并行区域前面已经被声明,但是在并行区域开始阶段,这个私有变量值变为未定义,在并行区域结束以后,也会变成未定义。例如:

int p = 0; 
// the value of p is 0
​
#pragma omp parallel private(p)
{
    // the value of p is undefined
    p = omp_get_thread_num();
    // the value of p is defined
    ...
}
// the value of p is undefined

所以为了顺从我们的直觉,我们尽量在并行区域内部定义变量。例如上面这段代码,我们可以变成

#pragma omp parallel
{
  int p = omp_get_thread_num();
  ...
}

这样的写法也能提高代码可读性。

Default

default有两个版本。我们先来看一下default(shared)

default(shared)

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都是共享变量

另外也可以将大多数变量默认为shared,将部分变量特指成private。

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 
}	

Default(none)

default(none)子句强制程序员指定所有变量的data sharing属性。

人在烦的时候,可能会瞎写出这些代码:

int n = 10;
std::vector<int> vector(n);
int a = 10;

#pragma omp parallel for default(none) shared(n, vector)
for (int i = 0; i < n; i++)
{
    vector[i] = i * a;
}

然后编译器就会报错:

error: ‘a’ not specified in enclosing parallel
        vector[i] = i * a;
                      ^
error: enclosing parallel
    #pragma omp parallel for default(none) shared(n, vector)

编译器吃出这里a没有被标明数据共享属性,修改成:

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

可以通过编译。


写在最后:

有两条规则

  1. 鼓励大家在写并行区域时候,使用default(none)子句,这样可以迫使程序员思考变量的数据类型应该是怎样的
  2. 尽可能在并行区域内声明私有变量,提高代码可读性,使得逻辑清晰

Links:

留下评论

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据