Oracle Solaris Studio 12.2: OpenMP API ユーザーガイド

第 5 章 タスク化

この章では、OpenMP 3.0 のタスク化モデルについて説明します。

5.1 タスク化モデル

OpenMP 仕様のバージョン 3.0 では、タスク化と呼ばれる新しい機能が追加されました。タスク化の機能を利用すると、再帰的な構造や while ループのように、作業単位が動的に生成されるようなアプリケーションの並列化が容易になります。

OpenMP では、明示的 タスクを task 指令を使って指定します。task 指令は、タスクとそのデータ環境に関連づけられたコードを定義します。タスクのコンストラクトは、プログラムのどこに置いても構いません。スレッドがタスクコンストラクトを検出すると、新しいタスクが生成されます。

スレッドがタスクコンストラクトを検出すると、スレッドはそれをすぐに実行するか、実行を延期して後で実行するかを選択することがあります。タスクの実行が保留されると、そのタスクは現在の並列領域に関連づけられた概念上のタスクプールに置かれます。現在のチームに属するスレッドは、プールからタスクを取り出し実行するという処理を、プールが空になるまで繰り返します。タスクを実行するスレッドは、タスクを検出した元のスレッドとは異なる場合があります。

タスクコンストラクトに関連づけられたコードは、1 回だけ実行されます。コードが最初から最後まで同じスレッドにより実行されると、タスクは結合されます。コードが複数のスレッドにより実行可能な場合は、タスクは結合解除されます。この場合、コードの異なる部分が別のスレッドにより実行されます。デフォルトでは、タスクは結合されていますが、結合解除節を task 指令で使用することにより、結合解除に指定することができます。

スレッドは、タスクのスケジューリングポイントでタスク領域の実行を中断して、異なるタスクを実行することができます。中断されたタスクが結合されている場合は、同じスレッドにより中断されたタスクの実行が後に再開されます。中断されたタスクが結合されていない場合は、現在のチームに属するスレッドならどれでもタスクの実行を再開できます。

OpenMP 仕様には、結合されたタスクに対して次のタスクスケジューリングポイントが定義されています。

Solaris Studio コンパイラによる実装に従い、上記のスケジューリングポイントは結合解除されたタスクのタスクスケジューリングポイントでもあります。

タスク指令を使って指定された明示的タスクに加え、OpenMP 仕様のバージョン 3.0 では暗黙的タスクの概念も取り入れられています。暗黙的タスクは、暗黙的な並列領域により生成されるタスク、または実行中に並列構文を検出したときに生成されるタスクです。それぞれの暗黙的タスクのコードは、parallel コンストラクトの内部コードです。暗黙的タスクはそれぞれチーム内の異なるスレッドに割り当てられ、結合されます。すなわち、暗黙的タスクは最初から最後まで常に最初に割り当てられたスレッドにより実行されます。

parallel コンストラクトが検出されたときに生成されたすべての暗黙的タスクは、マスタースレッドが並列領域の最後で暗黙的バリアーを終了するときに完了することが保証されます。一方、並列領域内に生成されるすべての明示的タスクは、並列領域内の次の暗黙的または明示的バリアーの終了時に完了することが保証されます。

if 節が task コンストラクトにあり、スカラー式の評価値が false の場合には、タスクを検出したスレッドはただちにそのタスクを実行する必要があります。if 節は、細かく組まれた多数のタスクを生成し、それらを概念上のプールに配置するというオーバーヘッドを避けるために使用することができます。

5.2 データ環境

task 指令は、タスクのデータ環境を定義する次のデータ属性節を取ります。

shared 節にリスト指定された変数へのタスク中のすべての参照は、 task 指令の直前に存在する同じ名前の変数を参照します。

private および firstprivate 変数のそれぞれに対し、新しいストレージが作成され、task コンストラクトの字句エクステントにある元の変数へのすべての参照は、新しいストレージへの参照に置き換えられます。firstprivate 変数は、タスクが検出された時点の元の変数値で初期化されます。

OpenMP 3.0 仕様のバージョン 3.0 (節 2.9.1) には、parallel、task、および worksharing 領域で参照される変数のデータ共有属性の決定方法が説明されています。

コンストラクト中で参照される変数のデータ共有属性は、事前定義明示的定義、または暗黙的定義のいずれかです。明示的に指定されたデータ共有属性を持つ変数は、指定されたコンストラクトの中で参照される変数で、コンストラクト上のデータ共有属性節にリストされています。暗黙的に指定されたデータ共有属性を持つ変数は、指定されたコンストラクトの中で参照される変数で、事前に決められたデータ共有属性を持たず、コンストラクト上のデータ共有属性節にリストがありません。

変数のデータ共有属性を暗黙的に決めるための規則は、必ずしも明白ではない場合があります。予期せぬ事態を避けるため、プログラミングの際には、OpenMP の暗黙的なスコープ宣言規則に依存せずに、データ共有属性節を使用してタスク構文で参照されるすべての変数を明示的にスコープ宣言することをお勧めします。

5.3 TASKWAIT 指令

指定された並列領域に結合されたすべての明示的タスクのサブセットの完了は、taskwait 指令の使用により指定される場合があります。taskwait 指令は、現在の (暗黙的または明示的) タスクの開始以降に生成された子タスクの完了を待つことを指定します。taskwait 指令は、直接の子タスクの完了を待つことを指定するもので、より下位のタスクすべてに対するものではない点に留意してください。

5.4 タスク化の例

次の C/C++ プログラムは、OpenMP タスクと taskwait 指令をどのように使うとフィボナッチ数列を再帰的に計算できるかを示したものです。

この例では、parallel 指令は 4 つのスレッドにより実行される並列領域を表しています。並列構文中で single 指令が使用され、1 つのスレッドだけが fib(n) を呼び出す print 文を実行することが示されています。

fib(n) を呼び出すと、task 指令に指定された 2 つのタスクが生成されます。一方のタスクは fib(n-1) を計算し、他方のタスクは fib(n-2) を計算します。2 つの返り値が加算され、fib(n) の返り値が求められます。fib(n-1) および fib(n-2) を呼び出すと、それぞれが 2 つのタスクを生成します。タスクは、fib() に渡された引数が 2 より小さくなるまで、再帰的に生成されます。

taskwait 指令は、fib() の呼び出しにより生成された 2 つのタスクの完了 (すなわち、タスクが ij を計算) が、fib() の呼び出しが戻る前に行われるようにします。

single 指令と fib(n) の呼び出しを行うスレッドが 1 つだけだったとしても、4 つのすべてのスレッドが生成されるタスクの実行に係わっている点に留意してください。

この例は、Solaris Studio 12.2 の 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 変数の値、またはスレッド番号など他のスレッド固有の情報がタスクスケジューリングポイントの前後で変更されたことを暗黙的に示しています。

中断されているタスクが結合されている場合は、タスクの実行を再開するスレッドは、中断したときのスレッドと同じになります。このため、スレッド番号はタスクの再開後も変更されません。ただし、threadprivate 変数の値は変更されることがあります。それは、スレッドは他のタスクの処理を行うようスケジューリングされることがあり、中断されたタスクを再開する前に threadprivate 変数が変更される場合があるからです。

中断されているタスクが結合解除場合は、タスクの実行を再開するスレッドは、中断したときのスレッドと異なる場合があります。このため、スレッド番号と 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 スタックデータへの参照

タスクには、タスクコンストラクトが現れるルーチンのスタック上のデータへの参照が付加されていることがよくあります。タスクの実行は次の暗黙的または明示的バリアーまで保留されることがあるため、指定されたタスクが現れるルーチンのスタックがポップされ、スタックデータが上書きされた後に、そのタスクが実行されることがあります。そのため、タスクに共有されたということでリストアップされたスタックデータが破棄されます。

必要な同期処理を挿入し、タスクが変数を参照したときに、変数が確実にスタック上にあるようにしておくことは、プログラマの責任です。2 つの例を次に示します。

最初の例では、itask コンストラクトの中で shared に指定されています。タスクは、work() のスタック上に割り当てられている i のコピーにアクセスします。

タスクの実行は保留されることがありますので、タスクは main() の並列領域の最後の暗黙的バリアーで、work() ルーチンの処理の終了後に実行されます。そのため、タスクが i を参照すると、その時にたまたまスタック上にあった値にアクセスしてしまうことになります。

正しい結果を得るためには、プログラマはタスクが完了する前に work() を終了しないようにしておく必要があります。taskwait 指令を task コンストラクトの後に挿入することにより、この処理を追加することができます。あるいは、task コンストラクトで、i に対して shared ではなく、firstprivate を指定することもできます。


例 5–3 スタックデータ: 例 1 - 正しくないバージョン


#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 スタックデータ: 例 1 - 修正されたバージョン


#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 を参照しています。このため、タスクは firstprivate 型のコピーである sections 構文中の j にアクセスします。これは、sections 構文のアウトラインルーチンのスタック上のローカル変数です (Solaris Studio コンパイラを含む一部の実装の場合)。

タスクを sections 領域の最後の暗黙的バリアーで sections コンストラクトのアウトラインルーチン終了後に実行するため、タスクの実行は保留されることがあります。このため、タスクから j が参照されるときには、スタック上の不確定の値がアクセスされてしまいます。

正しい結果を得るためには、プログラマは sections 領域が暗黙的バリアーに達する前にタスクが実行されるようにしておく必要があります。taskwait 指令を task コンストラクトの後に挿入することにより、この処理を追加することができます。あるいは、task コンストラクトで、j に対して shared ではなく firstprivate を指定することもできます。


例 5–5 例 2 - 正しくないバージョン


#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 例 2 - 修正されたバージョン


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