Sun Studio 12 Update 1: OpenMP API ユーザーズガイド

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

2 番目の例では、task コンストラクト中の j は、sections コンストラクト中の j と共有されています。このため、タスクは firstprivate 型のコピーである sections コンストラクト中の j にアクセスします。これは、sections コンストラクトのアウトラインルーチンのスタック上のローカル変数です (Sun 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);
 }

5.5.4 データスコープ属性

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

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

変数のデータ共有属性を暗黙的に決める方法についてのルールは、いつでも明確に決まっているわけではありません (「「5.2 データ環境」」の項を参照)。このため、予期せぬ事態を避けるため、プログラマは OpenMP の暗黙的なスコープルールに頼るのではなく、task コンストラクトの中で参照されるすべての変数に対して明示的にスコープを宣言 (defaultsharedprivate、および firstprivate 節を使用) することをお勧めします。