任务处理功能使 OpenMP 程序的复杂性有所增加。程序员需要特别注意带有任务的程序的工作原理。以下是一些需要考虑的编程问题。
当线程遇到任务调度点时,实现可能会选择暂停当前任务并安排线程处理另一个任务。这意味着 threadprivate 变量的值或线程特定的其他信息(如线程数)可能会在任务调度点发生变化。
如果暂停的任务为绑定 (tied) 任务,则恢复执行该任务的线程与暂停该任务的线程将是同一线程。因此,恢复该任务后,线程数将保持相同。但是,threadprivate 变量的值可能会更改,原因是可能会安排线程处理另一个任务,这样会在恢复暂停的任务之前修改 threadprivate 变量。
如果暂停的任务为非绑定 (untied) 任务,则恢复执行该任务的线程可能与暂停该任务的线程不同。因此,线程数和 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); }
任务可能会引用任务构造所在的例程的栈数据。由于任务的执行可能会延迟,直至下一个隐式或显式屏障,所以有可能出现这样的情况:给定的任务将在任务所在的例程的栈已经弹出,且栈数据被覆写(从而销毁由任务列为共享的栈数据)之后执行。
程序员应负责插入所需的同步,以确保任务引用变量时这些变量仍在栈中。以下是两个示例。
在第一个示例中,在 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 构造中的 j 引用 sections 构造中的 j。因此,任务会访问 sections 构造中 j 的 firstprivate 副本,该副本(在某些实现中,包括 Solaris 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); }