【问题标题】:Parallel Programming and C++并行编程和 C++
【发布时间】:2010-09-20 07:54:13
【问题描述】:

我最近写了很多关于并行计算和编程的文章,我确实注意到在并行计算方面出现了很多模式。注意到 Microsoft 已经发布了一个库以及 Microsoft Visual C++ 2010 社区技术预览版(名为 Parallel Patterns Library),我想知道您一直在使用和遇到哪些常见的并行编程模式可能值得记住?当您使用 C++ 编写并行程序时,您是否有任何遵循的习语和似乎不断出现的模式?

【问题讨论】:

  • 您能否说明您对哪种并行编程感兴趣?使用 MPI 的分布式编程与使用 OpenMP 的循环级并行有很大不同。
  • 我对并行编程中的一般模式和习语特别感兴趣——无论是在单台机器还是多台机器上的分布式内存或共享内存模型。

标签: c++ design-patterns parallel-processing idioms


【解决方案1】:

模式:

  • 生产者/消费者

    • 一个线程产生数据
    • 一个线程消耗数据
  • 循环并行

    • 如果你能证明每个循环都是独立的
      每次迭代都可以在一个单独的线程中完成
  • 重新绘制线程

    • 其他线程会工作并更新数据结构,但有一个线程会重新绘制屏幕。
  • 主事件线程

    • 多个线程可以生成事件
    • 一个线程必须处理事件(因为顺序很重要)
    • 应该尝试分离事件线程/重绘线程
      这(有助于)防止 UI 冻结
      但如果不小心操作,可能会导致过度重绘。
  • 工作组

    • 一组线程等待队列上的作业。
    • 线程从队列中提取一个工作项(如果没有可用则等待)。
      线程处理一个工作项直到完成
      完成后线程返回队列。

【讨论】:

  • 如果重绘线程在绘图时需要使用数据结构怎么办?我的意思是,当其他线程正在更新它们时它从它们中读取不是很危险吗?
  • 除非重绘线程与活动对象相关联,其中更改被“序列化”并且活动对象“拥有”修改的数据结构。
  • @leod:如果(且仅当)您认为它很关键,您的重绘线程可能会获得数据的读取锁定。大多数情况我认为不会太在意,如果数据正在更新,那么更新程序将很快发布更新事件以强制重新绘制。
  • 伯克利对parlab.eecs.berkeley.edu/wiki/patterns 进行了一些出色的研究。 PPL 的一些模式在以下书籍和 MSDN 上进行了描述,parallelpatternscpp.codeplex.com
【解决方案2】:

首先,您必须在共享内存计算和无共享计算之间做出选择。共享内存更容易,但不能很好地扩展 - 如果你也可以使用 shared-nothing

a) 拥有一个集群,而不是一个多处理器系统,或者

b) 如果你有很多 CPU(比如 > 60),并且内存高度不均匀

对于共享内存,常见的解决方案是使用线程;它们作为一个概念很容易理解,并且易于在 API 中使用(但难以调试)。

对于无共享,您使用某种消息传递。在高性能计算中,MPI 被建立为消息传递中间件。

然后,您还需要为并行活动设计架构。最常见的方法(同样因为它很容易理解)是农夫模式(又名主从)。

【讨论】:

  • 公平地说,您不必只选择一个——您可以创建一个支持两者的架构。但这些观点是正确的——你需要清楚你在哪里支持哪一个,因为需求(通常是设计)是完全不同的。
【解决方案3】:

并行执行模式

具有确定性模式的结构化并行编程是一种高级方法,主要基于循环并行执行模式的集合,通常称为算法骨架或并行构造,它抽象程序描述并隐藏低级多线程细节和许多复杂性程序员固有的并行性。

这些可重用的模式自动化了许多与范式相关的并行例程,例如同步、通信、数据分区或任务调度,并在内部处理它们。这种高级方法尝试了传统的低级线程锁模型,以更抽象和更简单的方式来表达并行性,并关注生产力和可编程性而不是性能。

有许多常用的模式,例如:Map-Reduce、Fork-Join、Pipeline 或 Parallel Loop...

论文

“具有确定性模式的结构化并行编程”是一篇讨论这些模式的论文。您还可以查看“MHPM:多尺度混合编程模型:灵活的并行化方法”,其中描述了这种名为 XPU 的方法的 C++ 实现。

图书馆

XPU 是一个基于任务的 C++ 库,由一组可重用的执行模式组成。它允许在单个同构编程模型内以多个粒度级别表达多种类型的并行性。它易于使用并说明了使用模式设计并行程序的兴趣。

例如它允许表达:

  1. 任务并行模式:

    简单或分层的 Fork/Join 执行模式,具有一些功能,例如 作为共享数据的自动检测和保护。

  2. 数据并行模式:

    具有可扩展数据分区的并行循环模式。

  3. 时间并行模式:

    管道执行模式。

【讨论】:

  • 能否提供一些您使用 XPU 库编写的代码示例?
【解决方案4】:

您已具备将并行性固定到程序部分的基础知识。 C++17 得到了很多(例如并行版本的 foreach、sort、find 和 friends、map_reduce、map、reduce、prefix_sum ...)请参阅 C++ Extensions for Parallelism

然后你有像延续这样的项目。想想std::future,但继续。实现这些的方法很少(boost 现在有一个很好的方法,因为 std 没有 next(...) 或 then(...) 方法,但最大的好处是不必等它做下一个任务

auto fut = async([]( ){..some work...} ).then( [](result_of_prev ){...more work} ).then... ;
fut.wait( );

后续任务之间缺乏同步很重要,因为任务/线程/...之间的通信会减慢并行程序的速度。

因此,基于任务的并行性非常好。使用任务调度程序,您只需传递任务并走开。他们可能有一些方法,比如信号量,来回传信息,但这不是强制性的。 Intel Thread Building BlocksMicrosoft Parallel Pattern Library 都有这方面的功能。

之后我们就有了 fork/join 模式。它并不意味着为每个任务创建 N 个线程。只是你有这些 N 个,理想情况下独立的事情要做(fork),并且当它们完成时在某处有一个同步点(join)。

auto semaphore = make_semaphore( num_tasks );
add_task( [&semaphore]( ) {...task1...; semaphore.notify( ); } );
add_task( [&semaphore]( ) {...task2...; semaphore.notify( ); } );
...
add_task( [&semaphore]( ) {...taskN...; semaphore.notify( ); } );
semaphore.wait( );

从上面你可以开始看到这是一个流程图的模式。未来是(A >> B >> C >> D),分叉连接是(A|B|C|D)。有了它,您可以将它们组合成一个图表。 (A1>>A2|B1>>B2>>B3|C1|D1>>D2>>(E1>>E2|F1)) 其中 A1>>A2 表示 A1 必须先于 A2 而 A|B 表示 A 和 B可以同时运行。速度较慢的部分位于图/子图的末尾。

目标是找到系统中不需要通信的独立部分。如上所述,并行算法几乎在所有情况下都比它们的顺序算法慢,直到工作负载变得足够高或大小变得足够大(假设通信不太健谈)。例如排序。在 4 核计算机上,您将获得大约 2.5 倍的性能,因为合并很麻烦,需要大量同步,并且在第一轮合并后不能工作所有核心。在 N 非常大的 GPU 上,可以使用效率较低的排序,比如 Bitonic,它最终会非常快,因为你有很多工作人员来完成工作,每个人都安静地做自己的事情。

减少通信的一些技巧包括,使用数组来获取结果,这样每个任务就不会尝试锁定对象来推送值。通常以后这些结果的减少会很快。

但是对于所有类型的并行性,速度慢来自于通信。减少它。

【讨论】:

    猜你喜欢
    • 2021-05-10
    • 2021-12-17
    • 1970-01-01
    • 2011-07-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多