タスク化により、OpenMP プログラムを複雑にする要素が加わります。プログラマは、タスクを使用するプログラムがどのように動作するかについて特別な注意を払う必要があります。プログラミング上、留意する必要のある項目について以下に説明します。
スレッドがタスクスケジューリングポイントを検出したときは、実装時に現在のタスクが中断され、そのスレッドが他のタスクを処理するようスケジュールされる設定となる場合があります。これは、threadprivate 変数の値、またはスレッド番号など他のスレッド固有の情報がタスクスケジューリングポイントの前後で変更されたことを暗黙的に示しています。
中断されているタスクが結合されている場合は、タスクの実行を再開するスレッドは、中断したときのスレッドと同じになります。このため、スレッド番号はタスクの再開後も変更されません。ただし、threadprivate 変数の値は変更されることがあります。それは、スレッドは他のタスクの処理を行うようスケジューリングされることがあり、中断されたタスクを再開する前に threadprivate 変数が変更される場合があるからです。
中断されているタスクが結合解除場合は、タスクの実行を再開するスレッドは、中断したときのスレッドと異なる場合があります。このため、スレッド番号と threadprivate 変数の値の両方とも、タスクスケジューリングポイントの前後で異なることがあります。
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); }
タスクには、タスクコンストラクトが現れるルーチンのスタック上のデータへの参照が付加されていることがよくあります。タスクの実行は次の暗黙的または明示的バリアーまで保留されることがあるため、指定されたタスクが現れるルーチンのスタックがポップされ、スタックデータが上書きされた後に、そのタスクが実行されることがあります。そのため、タスクに共有されたということでリストアップされたスタックデータが破棄されます。
必要な同期処理を挿入し、タスクが変数を参照したときに、変数が確実にスタック上にあるようにしておくことは、プログラマの責任です。2 つの例を次に示します。
最初の例では、i は task コンストラクトの中で 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 構文中の j が sections 構文中の 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); }