【问题标题】:Parallel Iterators并行迭代器
【发布时间】:2012-11-05 13:39:11
【问题描述】:

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

假设我想有一个方法可以对所有元素(节点)进行迭代。当然,这个迭代将被并行化。

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

【问题讨论】:

  • 共享数据的并行化很乏味。您应该仔细考虑您想要实现的目标以及如何实现它。您正在处理的数据到底是什么?一个消息队列,由一个写入,由另一个读取(这将是一个读取器/写入器问题)?还是您要并行反转的矩阵?所以换句话说:你的数据结构是什么,你的操作是什么?
  • 我的数据结构是动态图。它应该支持对所有节点和边的迭代,以及对节点邻居的迭代和广度优先搜索。需要支持图形粗化/收缩等修改。
  • 实现图是一个困难且容易出错的过程,为什么不看看一些库呢? Boost::graph 很有帮助,并且已经并行化了数据结构和算法
  • 同事和上级将 Boost:graph 描述为“近乎糟糕”,并推荐了一个自定义实现。不过,我应该看看图书馆。除了 Boost 之外还有什么?
  • 如果您在实现随机访问迭代器时遇到问题,您始终可以使用 OpenMP 3.0(及更高版本)task 构造来执行基于任务的并行性,而不是基于工作共享的并行性(即 OpenMP @ 987654322@构造)

标签: c++ iterator iteration openmp


【解决方案1】:

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

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

【讨论】:

  • 随机访问迭代器在这里可能很有用。即 operator+(into) 应该被定义。这使得 operator[] 不费吹灰之力。
  • 为什么随机访问迭代器与 OpenMP 结合比普通迭代器更好?
  • @cls:涉及非 RA 迭代器的循环不能并行运行。它们只是不受支持。
【解决方案2】:

让我扩展我的评论。除非您以跨平台兼容性为目标,并且您希望您的代码也可以编译和使用 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 区域内使用显式任务同步。

【讨论】:

    【解决方案3】:

    这是读写器问题的变体。

    这取决于该结构是否可变。如果没有,那么你就走吧,尽可能多地并行阅读。

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

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

    【讨论】:

      【解决方案4】:

      如果它们是树,您可能会想更多地考虑对 Euler Tour Traversals 的扫描,而不是“迭代器”。 http://en.wikipedia.org/wiki/Euler_tour_technique

      我希望我面前有 Stepanov 的书。我记得他曾短暂接触过它。

      【讨论】:

        【解决方案5】:

        我在 Java 中遇到过完全相同的问题。我实施的解决方案使用“hashmaps 的 hashmap”。我还是不明白为什么标准库不允许我们做多线程迭代器...... 您可以在此处阅读我的问题以及我的答案(带有指向 Java 代码的链接):

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

        【讨论】:

          猜你喜欢
          • 2010-11-16
          • 2014-07-08
          • 2013-01-21
          • 2013-06-15
          • 2015-08-24
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多