Sun Studio 12 Update 1:OpenMP API 用户指南

第 5 章 任务处理

本章介绍 OpenMP 3.0 任务处理模型。

5.1 任务处理模型

OpenMP 规范版本 3.0 引入了一个称为任务处理的新功能。任务处理功能有助于应用程序的并行化,其中任务单元是动态生成的,就像在递归结构或 while 循环中一样。

在 OpenMP 中,使用 task 指令指定显式任务。task 指令定义了与任务及其数据环境关联的代码。任务构造可以放置在程序中的任何位置,只要线程遇到任务构造,就会生成新任务。

当线程遇到任务构造时,可能会选择立即执行任务或延迟执行任务直到稍后某个时间再执行。如果延迟执行任务,则任务会被放置在与当前并行区域关联的概念任务池中。当前组中的线程会将任务从该池中取出,并执行这些任务,直到该池为空。执行任务的线程可能与最初遇到该任务的线程不同。

与任务构造关联的代码将只被执行一次。如果代码从始至终都由相同的线程执行,则任务为绑定 (tied) 任务。如果代码可由多个线程执行,使得不同的线程执行代码的不同部分,则任务为非绑定 (untied) 任务。缺省情况下,任务为绑定 (tied) 任务,可以通过将 untied 子句与 task 指令一起使用来将任务指定为非绑定 (untied) 任务。

为了执行不同的任务,允许线程在任务调度点暂停执行任务区域。如果暂停的任务为绑定 (tied) 任务,则同一线程稍后会恢复执行暂停的任务。如果暂停的任务为非绑定 (untied) 任务,则当前组中的任何线程都可能会恢复执行该任务。

OpenMP 规范为绑定 (tied) 任务定义了以下任务调度点:

在 Sun Studio 编译器中实现时,以上调度点也是非绑定 (untied) 任务的任务调度点。

除了使用 task 指令指定的显式任务外,OpenMP 规范版本 3.0 还介绍了隐式任务的概念。隐式任务是由隐式并行区域生成的任务,或是在执行期间遇到并行构造时生成的任务。每个隐式任务的代码都是 parallel 构造内的代码。每个隐式任务会分配给组中的不同线程,且隐式任务为绑定 (tied) 任务,即隐式任务从始至终总是由最初分配给的线程执行。

对于在遇到 parallel 构造时生成的所有隐式任务,都要保证在主线程退出并行区域末尾的隐式屏障时完成。另一方面,对于在并行区域中生成的所有显式任务,都要保证在从并行区域中的下一个隐式或显式屏障退出时完成。

task 构造中存在 if 子句,并且标量表达式的值计算为 false 时,遇到任务的线程必须立即执行任务。if 子句可用于避免生成许多细粒度任务以及将这些任务放在概念池中所造成的开销。

5.2 数据环境

task 指令采用以下数据属性子句,这些子句可定义任务的数据环境:

在任务内对 shared 子句中列出的变量的所有引用是指在 task 指令之前一看便知的同名变量。

对于每个 privatefirstprivate 变量,都会创建一个新存储,并且对 task 构造词法范围内的原始变量的所有引用都会被对新存储的引用所替换。遇到任务时,将会使用原始变量的值初始化 firstprivate 变量。

对于未在 task 构造的数据属性子句中列出以及未根据 OpenMP 规则预先确定的变量的数据共享属性,将按如下所述隐式确定:

由此可见:

5.3 TASKWAIT 指令

可以通过使用 taskwait 指令指定绑定到给定并行区域的所有显式任务的子集完成时的情况。taskwait 指令指定在完成自当前(隐式或显式)任务开始以来生成的子任务时进行等待。请注意,taskwait 指令指定在完成直接子任务(而不是所有后续任务)时进行等待。

5.4 任务处理示例

以下 C/C++ 程序说明为什么 OpenMP 任务和 taskwait 指令可用于递归计算斐波纳契数。

在该示例中,parallel 指令指示一个将由四个线程执行的并行区域。在并行构造中,single 指令用于指示只有其中一个线程将执行调用 fib(n)print 语句。

fib(n) 的调用会生成两个任务(由 task 指令指示)。其中一个任务计算 fib(n-1),另一个任务计算 fib(n-2),将返回值加在一起即可产生由 fib(n) 返回的值。对 fib(n-1)fib(n-2) 的每个调用反过来又会生成两个任务。将会以递归方式生成任务,直到传递到 fib() 的参数小于 2。

taskwait 指令可确保在调用 fib() 的过程中生成的两个任务(即计算 ij 的任务)在对 fib() 的调用返回之前已完成。

请注意,虽然只有一个线程执行 single 指令(因而也只有一个线程对 fib(n) 进行调用),但是所有四个线程都将参与执行生成的任务。

该示例是使用 Sun Studio 12 Update 1 C++ 编译器编译的。


示例 5–1 任务处理示例:计算斐波纳契数


#include <stdio.h>
#include <omp.h>
int fib(int n)
{
  int i, j;
  if (n<2)
    return n;
  else
    {
       #pragma omp task shared(i) firstprivate(n)
       i=fib(n-1);

       #pragma omp task shared(j) firstprivate(n)
       j=fib(n-2);

       #pragma omp taskwait
       return i+j;
    }
}

int main()
{
  int n = 10;

  omp_set_dynamic(0);
  omp_set_num_threads(4);

  #pragma omp parallel shared(n)
  {
    #pragma omp single
    printf ("fib(%d) = %d\n", n, fib(n));
  }
}


% CC -xopenmp -xO3 task_example.cc
% a.out
fib(10) = 55

5.5 编程注意事项

任务处理功能使 OpenMP 程序的复杂性有所增加。程序员需要特别注意带有任务的程序的工作原理。以下是一些需要考虑的编程问题。

5.5.1 THREADPRIVATE 和线程特定的信息

当线程遇到任务调度点时,实现可能会选择暂停当前任务并安排线程处理另一个任务。这意味着 threadprivate 变量的值或线程特定的其他信息(如线程数)可能会在任务调度点发生变化。

如果暂停的任务为绑定 (tied) 任务,则恢复执行该任务的线程与暂停该任务的线程将是同一线程。因此,恢复该任务后,线程数将保持相同。但是,threadprivate 变量的值可能会更改,原因是可能会安排线程处理另一个任务,这样会在恢复暂停的任务之前修改 threadprivate 变量。

如果暂停的任务为非绑定 (untied) 任务,则恢复执行该任务的线程可能与暂停该任务的线程不同。因此,线程数和 threadprivate 变量的值在任务调度点之前和之后都可能不同。

5.5.2 锁

OpenMP 3.0 指定,锁不再归线程所有,而是归任务所有。一旦获取了锁,当前任务就会拥有该锁,同一任务必须先释放锁才能完成任务。

另一方面,critical 构造仍保留采用基于线程的互斥机制

使用锁时,需要格外小心锁拥有权的变化。以下程序(在 OpenMP 规范版本 3.0 中作为示例 A.43.1c 出现)符合 OpenMP 2.5,因为在并行区域中释放锁 lck 的线程与在该程序顺序部分中获取锁的线程为同一线程(并行区域的主线程与初始线程相同)。但是,该程序不符合 OpenMP 3.0,因为释放锁 lck 的任务区域与获取锁的任务区域不同。


示例 5–2 使用锁的示例:不符合 OpenMP 3.0


#include <stdlib.h>
#include <stdio.h>
#include <omp.h>

int main()
{
  int x;
  omp_lock_t lck;

  omp_init_lock (&lck);
  omp_set_lock (&lck);
  x = 0;

  #pragma omp parallel shared (x)
  {
    #pragma omp master
    {
      x = x + 1;
      omp_unset_lock (&lck);
    }
  }
  omp_destroy_lock (&lck);
}

5.5.3 对栈数据的引用

任务可能会引用任务构造所在的例程的栈数据。由于任务的执行可能会延迟,直至下一个隐式或显式屏障,所以有可能出现这样的情况:给定的任务将在任务所在的例程的栈已经弹出,且栈数据被覆写(从而销毁由任务列为共享的栈数据)之后执行。

程序员应负责插入所需的同步,以确保任务引用变量时这些变量仍在栈中。以下是两个示例。

在第一个示例中,在 task 构造中将 i 指定为 shared,任务会访问在 work() 的栈中分配的 i 的副本。

任务的执行可能会延迟,使得任务将在 work() 例程已返回后,在 main() 中的并行区域末尾的隐式屏障处执行。因此当任务引用 i 时,会访问当时碰巧在栈中的某个不确定的值。

为了得到正确的结果,程序员需要确保 work() 不会在任务完成前退出。这可以通过在 task 构造之后插入 taskwait 指令来实现。或者,可以在 task 构造中将 i 指定为 firstprivate 而不是 shared


示例 5–3 栈数据:第一个示例-不正确的版本


#include <stdio.h>
#include <omp.h>
void work()
 {
   int i;

   i = 10;
   #pragma omp task shared(i)
   {
     #pragma omp critical
     printf("In Task, i = %d\n",i);
   }
 }

int main(int argc, char** argv)
 {
    omp_set_num_threads(8);
    omp_set_dynamic(0);

    #pragma omp parallel 
    {
      work();
    }
 }


示例 5–4 栈数据:第一个示例-更正的版本


#include <stdio.h>
#include <omp.h>

void work()
 {
   int i;

   i = 10;
   #pragma omp task shared(i)
   {
     #pragma omp critical
     printf("In Task, i = %d\n",i);
   }

   /* Use TASKWAIT for synchronization. */
   #pragma omp taskwait
 }

int main(int argc, char** argv)
 {
    omp_set_num_threads(8);
    omp_set_dynamic(0);

    #pragma omp parallel 
    {
      work();
    }
 }

在第二个示例中,task 构造中的 jsections 构造中的 j 共享。因此,任务会访问 sections 构造中 jfirstprivate 副本,该副本(在某些实现中,包括 Sun Studio 编译器)为 sections 构造的概要例程的栈中的局部变量。

任务的执行可能会延迟,使得任务将在 sections 构造的概要例程退出后,在 sections 区域末尾的隐式屏障处执行。因此当任务引用 j 时,会访问栈中的某个不确定的值。

为了得到正确的结果,程序员需要确保任务在 sections 区域达到其隐式屏障前执行。这可以通过在 task 构造之后插入 taskwait 指令来实现。或者,可以在 task 构造中将 j 指定为 firstprivate 而不是 shared


示例 5–5 第二个示例-不正确的版本


#include <stdio.h>
#include <omp.h>

int main(int argc, char** argv)
 {
    omp_set_num_threads(2);
    omp_set_dynamic(0);
    int j=100;

    #pragma omp parallel shared(j)
    {
       #pragma omp sections firstprivate(j)
       {
          #pragma omp section
          {
             #pragma omp task shared(j)
             {
               #pragma omp critical
               printf("In Task, j = %d\n",j);
             }
          }
       }
    }

    printf("After parallel, j = %d\n",j);
 }


示例 5–6 第二个示例-更正的版本


#include <stdio.h>
#include <omp.h>

int main(int argc, char** argv)
 {
    omp_set_num_threads(2);
    omp_set_dynamic(0);
    int j=100;

    #pragma omp parallel shared(j)
    {
       #pragma omp sections firstprivate(j)
       {
          #pragma omp section
          {
             #pragma omp task shared(j)
             {
               #pragma omp critical
               printf("In Task, j = %d\n",j);
             }

             /* Use TASKWAIT for synchronization. */
             #pragma omp taskwait
          }
       }
    }

    printf("After parallel, j = %d\n",j);
 }

5.5.4 数据作用域属性

OpenMP 3.0 规范版本 3.0(第 2.9 节中)介绍了如何确定在并行、任务和工作共享区域中引用的变量的数据共享属性。

构造中引用的变量的数据共享属性可以是以下属性之一:预先确定显示确定隐式确定。具有显式确定数据共享属性的变量是那些在给定构造中引用,并在构造的数据共享属性子句中列出的变量。具有隐式确定数据共享属性的变量是那些在给定构造中引用、不具有预先确定数据共享属性,并且不在构造的数据共享属性子句中列出的变量。

有关如何隐式确定变量的数据共享属性的规则可能并不总是很直观(请参见5.2 数据环境)。因此我们建议,要避免出现任何让人感到惊讶的现象,程序员应显式确定任务构造中引用的所有变量的作用域(使用 defaultsharedprivatefirstprivate 子句),而不是依赖于 OpenMP 隐式作用域规则。