并行迭代器

时间:2012-11-05 13:39:11

标签: c++ iterator iteration openmp

我正在设计一个C ++数据结构(用于图表),它将由并行代码(使用OpenMP)使用。

假设我想要一个能够迭代所有元素(节点)的方法。当然,这个迭代将被并行化。

是否可以为此目的使用迭代器?迭代器应该如何实现并行访问?在这种情况下,您是建议支持还是反对使用迭代器?

5 个答案:

答案 0 :(得分:6)

OpenMP并行循环不能很好地与迭代器一起使用。您需要在图类上实现索引机制(operator[]采用积分参数)。

如果您确实想使用OpenMP 3.0迭代器支持,请确保您具有随机访问迭代器。将其实现为指向节点或边缘的指针是最简单的选择。

答案 1 :(得分:3)

让我扩展一下我的评论。除非您的目标是跨平台兼容性,并且您希望您的代码也能编译和使用MS Visual C ++,否则您可以通过使用显式OpenMP任务来抵消向图形对象提供“线性”迭代器的复杂性。 OpenMP 3.0中引入了显式任务(因此,MSVC不支持它,即使在2012年也只符合更早的规范)。任务是可以并行执行的代码块。它们由task构造创建:

... other code ...
#pragma omp task
{
   ... task code ...
}
... other code ...

每当执行流到达标记区域时,都会创建一个新的任务对象并将其放入任务队列。然后在某些时间点,空闲线程从队列中获取一个任务并开始执行它。任务与OpenMP部分非常相似,因为它们继承了它们的环境,并且它们可以以与代码的串行版本不同的顺序运行。

使用任务可以实现递归算法,也可以轻松使用不提供随机迭代器的C ++容器。例如,可以使用任务执行遍历二叉树的遍历:

// Helper routine to traverse a tree and create one task for each node
void traverse_and_make_tasks(tree_node *tn)
{
   if (tn == NULL)
      return;

   // Create a task to process the node
   #pragma omp task
   {
      very_long_computation(tn->value);
   }

   traverse_and_make_tasks(tn->left);
   traverse_and_make_tasks(tn->right);
}

... main code ...

// Disable dynamic teams
omp_set_dynamic(0);

#pragma omp parallel
{
   #pragma omp single nowait
   {
      traverse_and_make_tasks(tree->root_node);
   }
}

(需要禁用动态团队以防止OpenMP运行时过于聪明并单线程执行parallel区域)

这是一种非常常见的任务模式,称为单/串行生成器。每当执行进入parallel区域时,单个线程就会执行single构造中的代码。它使用三者的根节点调用traverse_and_make_taskstraverse_and_make_tasks遍历三个并为每个节点创建一个任务。 task构造仅在parallel区域内使用(静态作用域)或在parallel区域(动态作用域)内部(直接或间接)调用的代码中使用时才有效。当traverse_and_make_tasks遍历树时,它会生成排队的任务。在parallel区域的末尾有一个隐式任务调度点,这大致意味着在执行所有任务之前执行不会恢复到区域末尾。也可以使用#pragma omp taskwait在并行区域内放置显式点。它与barrier的工作方式类似 - 执行“阻止”,直到所有任务都被处理完毕。

另一种常见模式是并行生成器,其中任务是并行生成的。通过对traverse_and_make_tasks

的简单修改,可以轻松地将上面的示例代码转换为并行生成器
void traverse_and_make_tasks(tree_node *tn)
{
   if (tn == NULL)
      return;

   #pragma omp task
   traverse_and_make_tasks(tn->left);
   #pragma omp task
   traverse_and_make_tasks(tn->right);

   // Create a task to process the node
   very_long_computation(tn->value);
}

此版本的代码在每个节点创建两个任务 - 一个用于处理左后代,另一个用于处理右后代。如果这是串行代码,它将从下往上遍历树。但是,在并行情况下,任务排队会导致或多或少的从上到下的遍历。

还有许多其他可能的使用任务的方案。也可以在非递归的情况下使用它们来处理不提供随机迭代器的容器,这是工作共享构造for所需的:

typedef container_type::iterator citer;

container_type container;
... push some values in the container ...

#pragma omp parallel
{
   #pragma omp single nowait
   {
      for (citer it = container.begin(); it != container.end(); it++)
         #pragma omp task
         process(*it);
   }

   #pragma omp taskwait

   // process more
   #pragma omp single nowait
   {
      for (citer it = container.begin(); it != container.end(); it++)
         #pragma omp task
         process_more(*it);
   }
}

此示例还说明了在parallel区域内使用显式任务同步。

答案 2 :(得分:1)

这是读者/作者问题的变体。

这取决于该结构是否可变。如果没有,那么关闭你去,并行阅读你想要的。

但是如果它是可变的,那么迭代器可能会与对结构所做的更改发生冲突。例如,迭代器可能会到达当前正在删除的元素。一种解决方案是为每个迭代器创建一个只读的,不可变的结构副本,但是迭代器在创建迭代器之后不会注册对结构所做的任何更改。第二种解决方案是进行写时复制实现,这将导致对结构的所有写入创建一个新对象,当前运行的迭代器在旧对象上运行。

您需要根据算法确定对该结构的写入对程序的意义,然后适当地实现读/写锁定。

答案 3 :(得分:1)

如果它们是树木,您可能希望在Euler Tour Traversals上的扫描比“迭代器”更多。 http://en.wikipedia.org/wiki/Euler_tour_technique

我希望我面前有Stepanov的书。我记得他简单地接触过它。

答案 4 :(得分:0)

我在Java中遇到了完全相同的问题。我实现的解决方案使用“散列图的散列图”。我仍然不明白为什么标准库不让我们做多线程迭代器... 您可以在此处阅读我的问题以及我的答案(链接到Java代码):

Scalable way to access every element of ConcurrentHashMap<Element, Boolean> exactly once