有并行和无并行的OpenMP任务

8
Node *head = &node1;
while (head)
{
    #pragma omp task 
        cout<<head->value<<endl;
    head = head->next;
}

#pragma omp parallel
{
    #pragma omp single
    {
        Node *head = &node1;
        while (head)
        {
            #pragma omp task 
                cout<<head->value<<endl;
            head = head->next;
        }
    }
}

在第一个代码块中,我只是创建了没有并行指令的任务。而在第二个代码块中,我使用了并行指令和单一指令,这种方法在论文中经常见到。 我想知道它们之间有什么区别?顺便说一下,我知道这些指令的基本含义。
我评论中的代码:
void traverse(node *root)
{
    if (root->left) 
    {
        #pragma omp task 
        traverse(root->left);
    }
    if (root->right) 
    {
        #pragma omp task 
        traverse(root->right);
    }
    process(root);
}
1个回答

14

区别在于,在第一个代码块中,您实际上没有创建任何任务,因为该代码块本身不是嵌套在活动并行区域内(无论在语法上还是词法上都不是)。在第二个代码块中,task 构造语法上被嵌套在 parallel 区域内,并且如果该区域在运行时处于活动状态(活动的并行区域是指以多个线程执行的区域),则会排队显式任务。词法嵌套不太明显。请看以下示例:

void foo(void)
{
   int i;

   for (i = 0; i < 10; i++)
      #pragma omp task
      bar();
}

int main(void)
{
   foo();

   #pragma omp parallel num_threads(4)
   {
      #pragma omp single
      foo();
   }

   return 0;
}

第一次调用foo()发生在任何并行区域之外。因此,task指令几乎不起作用,所有对bar()的调用都是串行的。第二次调用foo()来自于并行区域内部,因此新的任务将在foo()内生成。parallel区域是活动的,因为通过num_threads(4)子句将线程数固定为4

OpenMP指令的这种不同行为是一种设计特性。主要思想是编写既可作为串行也可作为并行执行的代码。

尽管如此,在foo()中存在task结构会对代码进行一些转换,例如将foo()转换成以下形式:

void foo_omp_fn_1(void *omp_data)
{
   bar();
}

void foo(void)
{
   int i;

   for (i = 0; i < 10; i++)
      OMP_make_task(foo_omp_fn_1, NULL);
}

这里的OMP_make_task()是来自OpenMP支持库的一个假设函数(不公开),它将作为其第一个参数提供的函数调用排入队列。如果OMP_make_task()检测到它在非并行区域外运行,它会直接调用foo_omp_fn_1()。这会给串行情况下对bar()的调用增加一些开销。与main -> foo -> bar不同,调用变成了main -> foo -> OMP_make_task -> foo_omp_fn_1 -> bar。这意味着较慢的串行代码执行。

这在工作共享指令中更加明显:

void foo(void)
{
   int i;

   #pragma omp for
   for (i = 0; i < 12; i++)
      bar();
}

int main(void)
{
   foo();

   #pragma omp parallel num_threads(4)
   {
      foo();
   }

   return 0;
}

第一次调用 foo() 会以串行方式运行循环。第二次调用将12个迭代分配给4个线程,即每个线程只执行3个迭代。再次使用一些代码转换技巧来实现这一点,串行循环的速度会比在foo()中没有 #pragma omp for 的情况下慢。

这里的教训是永远不要在不真正需要的地方添加 OpenMP 结构。


似乎我在任务的使用上犯了一个错误。这个问题是因为我看到了一个只有“任务”递归遍历树的代码,所以我在问题中添加了它。我猜想在调用遍历函数时应该有“并行”和“单一”来封闭它。非常感谢您真诚的回答。 - Annie Kim
@AnnieKim,是的,如你所问的traverse()函数将在活动的parallel区域内被调用时并行遍历树,在其他情况下则串行遍历。这就是OpenMP的美妙之处 :) (尽管增加了开销) - Hristo Iliev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接