タスク化により、OpenMP プログラムを複雑にする要素が加わります。このセクションでは、タスク関連のプログラミングで考慮する問題について説明します。
スレッドがタスクスケジューリングポイントを検出すると、実装時に現在のタスクが中断され、そのスレッドが別のタスクを処理するようにスケジュールされる場合があります。この動作は、タスク内の threadprivate 変数またはほかのスレッド固有の情報 (スレッド番号など) がタスクスケジューリングポイントの前後で変わる可能性があることを意味します。
中断されたタスクが結合されている場合、タスクの実行を再開するスレッドは、中断したときのスレッドと同じになります。このため、スレッド番号はタスクの再開後も変更されません。ただし、場合によっては threadprivate 変数の値が変わることがあります。スレッドが中断されたタスクを再開する前に、threadprivate 変数を変更する別のタスクを処理するようにスケジュールされていた可能性があるからです。
中断されたタスクが結合解除されている場合、タスクの実行を再開するスレッドは、中断したときのスレッドとは異なる場合があります。このため、スレッド番号と threadprivate 変数の値のどちらも、タスクスケジューリングポイントの前後で異なる可能性があります。
OpenMP 3.0 以降、ロックの所有者はスレッドではなくタスクです。タスクによってロックが取得されると、そのタスクがロックを所有します。タスクの完了前に、同じタスクがそのロックを解放する必要があります。ただし、critical 構文は、スレッドベースの相互排他メカニズムのままです。
ロックはタスクによって所有されるので、ロックを使用する際は十分に注意してください。次の例は OpenMP 2.5 仕様に準拠しています。並列領域でロック lck を解放するスレッドが、プログラムの順次処理部分でそのロックを取得したのと同じスレッドであるためです。並列領域のマスタースレッドと初期のスレッドは同一です。ところが、この例はそれ以降の仕様には準拠していません。ロック lck を解放するタスク領域が、そのロックを取得したタスク領域と異なるためです。
使用例 9 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);
}
タスクは、task 構文が使用されているルーチン (ホストルーチン) のスタック上のデータを参照することがあります。タスクの実行は次の暗黙または明示バリアまで延期されることがあるため、ホストルーチンのスタックがポップされ、スタックデータが上書きされたあとでタスクが実行される可能性があり、それによって、タスクが参照したスタックデータが破棄されます。
このセクションの 2 つの例に示すように、必要な同期処理を挿入して、タスクが変数を参照したときに、変数が確実にスタック上にあるようにしておいてください。
使用例 10では、i が task 構文で shared になるように指定されるため、タスクは、work() ルーチンのスタック上に割り当てられている i のコピーにアクセスします。
タスクの実行は延期されることがあるため、タスクは main() の並列領域の終わりにある暗黙バリアで、work() ルーチンの復帰後に実行されます。その時点で、タスクが i を参照すると、そのときにたまたまスタック上にあった値にアクセスしてしまいます。
正しい結果が得られるよう、タスクが完了する前に work() が復帰しないようにしてください。そのためには、使用例 11に示すように、taskwait ディレクティブを task 構文のあとに挿入します。あるいは、task 構文で、i を shared ではなく、firstprivate になるように指定することもできます。
使用例 10 スタックデータ: 正しくない参照#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();
}
}
使用例 11 スタックデータ: 修正された参照
#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 にアクセスします。これは、Oracle Developer Studio では、sections 構文のアウトラインルーチンのスタック上のローカル変数です。
タスクの実行は延期されることがあり、タスクが sections 領域の終わりにある暗黙バリアで、sections 構文のアウトラインルーチンの終了後に実行されることがあります。そのため、タスクが j を参照すると、スタック上の不確定の値にアクセスしてしまいます。
正しい結果を得るためには、使用例 13に示すように、taskwait ディレクティブを task 構文のあとに挿入して、sections 領域がその暗黙バリアに到達する前にタスクが実行されるようにしてください。あるいは、task 構文で、j を shared ではなく firstprivate になるように指定することもできます。
使用例 12 sections データ: 正しくない参照#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);
}
}
} /* Implicit barrier for sections */
} /* Implicit barrier for parallel */
printf("After parallel, j = %d\n",j);
}
使用例 13 sections データ: 修正された参照
#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
}
} /* Implicit barrier for sections */
}/* Implicit barrier for parallel */
printf("After parallel, j = %d\n",j);
}