OpenMP——共享编程知识点

  OpenMP是一个库,是针对共享内存并行编程的API。

  OpenMP被明确地设计成可以用来对已有的穿行程序进行增量式并行化,这对于MPI是不可能的,对于Pthreads也是相当困难的。

  这里的OpenMP学习这几部分:1)对源代码进行少量改动就可以并行化许多串行的for循环。2)任务并行化。3)显式线程同步。4)缓存对共享内存编程的影响(这是共享内存编程中的标准问题)。5)当串行代码(特别是一个串行库)被一个共享内存程序使用时遇到的问题。

 

1.入门:多线程执行

  我们想多线程执行Hello()函数,需要这样写:

  #pragma omp parallel num_threads(thread_count)

    Hello();

  ...

  return 0;


  当代码块执行完,即线程从Hello调用中返回时,有一个隐式路障,这意味着完成代码块的线程将等待线程组中的所有其他线程完成代码块。当左右线程都完成了代码块,从线程将终止,主线程将继续执行之后的代码,此例子中,执行 ... return 0.

  而在Hello函数内部,需要得到当前执行自己的线程号,所以内部是这样的:

  void Hello(void){

   int my_rank = omp_get_thread_num();

   int thread_count = omp_get_num_threads();

   ...

  }

2.竞争条件(race condition)和临界区(critical section)

  race condition:多线程试图访问一个共享资源,并且至少一个访问是更新该共享资源。

  critical section: 引起竞争条件的代码。即global_result += my_result 是一个被多个更新共享资源的线程执行的代码,并且共享资源一次只能被一个线程更新。

  程序例子:多线程求解梯形面积。每个线程求一组小梯形,然后把自己求得的那部分面积加在一起(global_result).

  OpenMP中使用critical指令保证互斥:

#pragma omp parallel critical

 global_result += my_result;

3.变量的作用域

  共享作用域:能被线程组中所有线程访问的变量称为拥有共享作用域。

  私有作用域:只能被单个线程访问的变量称为拥有私有作用域。

4.归约子句

  归约操作符(reduction operator):一个二元操作就叫做一个归约操作符(例如加和减)。

  归约(reduction)就是将相同的归约操作符重复应用到操作数序列来得到一个结果的计算。
  归约变量:归约过程中所有操作的中间结果存储在同一个变量里,这个变量称为归约变量。

  对一个数组的值进行求和运算就是一个归约:重复应用加法这个归约操作符于数组中得到一个结果 sum。

  OpenMP中可以指定一个归约变量来表示归约的结果。to do this ,我们需要在parallel指令中添加一个reduction子句:

 global_result = 0.0;

# pragma omp parallel num_threads(thread_count) reduction(+:global_result)

 global_result += Local_trap(double a, double b, int n);

  通过reduction子句,OpenMP给每个线程有效地创建了一个私有变量,运行时系统在这个私有变量中存储每个线程的结果。OpenMP也创建了一个临界区,并且在这个临界区中,将存储在私有变量中的值进行相加。

  以上代码的执行效果与下面这部分相同:

global_result = 0.0;
#pragma omp parallel num_threads(thread_count) 
 {
  double my_result = 0.0;
  my_result +=Local_trap(double a ,double b, int n);
#pragma omp parallel critical 
  global_result += my_result;
 }

5.parallel for指令

  parallel for 指令生成一组线程来执行后面的结构化代码块。

  运用parallel for指令,系统通过在线程间划分循环(默认系统是块划分)来并行化for循环。这与parallel指令非常不同,因为在parallel指令之前的块,一般来说其工作必须由线程本身在线程之间划分。

  关于作用域,在parallel指令中,所有变量(函数列表里的变量)缺省作用域是共享的。在parallel指令中是私有的,因为如果循环变量私有,那么变量更新 i++ 也会是一个无保护的临界区,因此循环变量私有,每个线程都有自己的i的副本。

6.parallel for 的局限

  能够并行的的for循环必须确定迭代次数,而且迭代还一定得是这样确认的:

  1)只由for语句本身(即for(..., ..., ...))来确定。

for(i = 0; i < n; i++){
 if(...) break;
 ...
}

 这样是不能并行的,因为迭代次数不能只由for语句决定。

  2)在循环之前确定。

  ps:for里面的循环变量 i 必须是整型或者指针类型。还有其他一些限制(P151).

7.数据依赖性

  数据依赖/循环依赖(loop-carried dependence):在循环迭代中的计算依赖于一个或者更多个先前的迭代结果。例如计算前n个斐波那契(Fibonacci)数。

8.循环依赖的程序例子——π值的估计

  pi = 4[ 1 - 1/3 + 1/5 + 1/7 + ... ] = 4Σ(-1)k/(2k+1), k>=0

  串行代码:

double factor = 1.0 //类型是double, 而不是int 或 long,这点很重要
double sum = 0.0;
for(k = 0; k < n; k++){
 sum += factor/(2*k+1);
 factor = -factor;
}
pi_approx = 4.0*sum;
  我们可能首先会这么并行(naive,但居然还知道用个reduction子句注意临界区):
double factor = 1.0 
double sum = 0.0;
#pragma omp parallel for num_threads(thread_count) reduction (+:sum)//只加了一行代码
for(k = 0; k < n; k++){
 sum += factor/(2*k+1);
 factor = -factor;
}
pi_approx = 4.0*sum;

  第k次的factor的更新和第k+1次迭代对sum的累加是一个循环依赖。如果第k次迭代被分配给一个线程,而第k+1次迭代被分配给另一个线程,则我们不能保证factor的值(符号)是正确的。

  问题修复:检查系数。

  注意到k是偶数,那么factor是+1;k是奇数,则值是-1;

  我们将有依赖问题的两句

 sum += factor/(2*k+1);
 factor = -factor;

   替换为:

  if(k%2 == 1.0)
   factor = 1.0;
  else 
   factor = -1.0;
  sum += factor/(2*k + 1);
  就消除了循环依赖性. 所以现在的代码是这样的:
double factor = 1.0 //类型是double, 而不是int 或 long,这点很重要
double sum = 0.0;
#pragma omp parallel for num_threads(thread_count)\
  reduction (+:sum)
for(k = 0; k < n; k++){
  if(k%2 == 0)
   factor = 1.0;
  else 
   factor = -1.0;
  sum += factor/(2*k + 1);
}
pi_approx = 4.0*sum;

  就在你舒一口气的时候,不幸的事情发生了,这代码还是会出错的!错在哪里?——数据依赖缺失解除了,sum也设置了临界区,然而对共享变量的处理仍然不够谨慎!factor作为共享变量存在问题:thread 0给它赋值1,还没来得及用,factor被thread 1 赋值成-1了,此时thread 0 才开始计算sum,那么factor的值就已经错了!在这里,我们还需要保证每个线程有自己的factor副本,就是说,为了使得代码正确,我们需要保证factor有私有作用域。通过添加一个private子句到parallel for 指令中来实现这一目标。即:
#pragma omp parallel for num_threads(thread_count) \
    reduction(+:sum) private(factor)
  这样终于修改完了~

double factor = 1.0;
double sum = 0.0;
#pragma omp parallel for num_threads(thread_count) \
  reduction(+:sum) private(none)
  for(int k = 0; k < n;  k++){
   if(k%2 == 0)
    factor = 1.0;
   else
    factor = -1.0;
   sum += factor/(2*k+1);
  }

  note:一个私有作用域的变量的值在parallel block或者parallel for block的开始处和block完成之后都是未指定的!(例子在P154 累了不打了)

9.关于作用域的更多问题

  关于变量factor的问题是常见问题中的一个,我们通常需要考虑在parallel块或parallel for块中的每个变量的作用域。因此,与其让OpenMP决定每个变量的作用域,还不如让程序员来干。事实上,OpenMP提供了一个子句default,它显式的要求我们这样做。如果我们添加子句 default(none) 到parallel或者parallel for指令中,那么编译器将要求我们明确在这个块中使用的每一个变量和已经在块外声明的变量的作用域。(在块中声明的变量都是私有的,这点我们之前已经说过了)

  如果我们使用了 default(none), 那么对π的计算是这样的:

double factor = 1.0;
double sum = 0.0;
#pragma omp parallel for num_threads(thread_count) \
  default(none) reduction(+:sum) private(k, factor) \
  shared(n) 
  for(int k = 0; k < n;  k++){
   if(k % 2 == 0)
    factor = 1.0;
   else
    factor = -1.0;
   sum += factor/(2*k+1);
  }










你可能感兴趣的:(并行计算)