多核编程:难点

14

我正在编写一本关于使用.NET 4进行多核编程的书,我很好奇人们在多核编程方面觉得难以理解或预计难以理解的部分是什么?


当您编辑帖子时,右下角有一个“社区维基”复选框。 - kennytm
3
如果您不想的话,就不必在您的帖子中添加Wiki链接。Wiki意味着“任何人都可以编辑”,并且您希望社区承担您发布内容的所有权,但它不用于分类“主观”或“讨论”问题,因为我们已经有标签来做这件事。 - Juliet
2
我认为那些没有正确答案的问题是成为社区维基的好候选者,因为这也让人们编辑答案。 - Gabe
说实话,我太菜了,甚至不知道自己哪些地方不懂 :) - Benjol
7个回答

7

什么是一个有用的工作单元可以并行化,如何找到/组织一个?

所有这些并行计算原语都是无用的,如果你分叉了一个小于分叉开销的工作,那么实际上会给你带来一个不错的减速而不是你期望的加速。

因此,其中一个大问题是找到显然比并行计算原语更昂贵的工作单元。这里的一个关键问题是,没有人知道任何执行的成本,包括并行计算原语本身。显然,校准这些成本将非常有帮助。(顺便说一下,我们设计、实现和日常使用一个并行编程语言PARLANSE,其目标是通过允许编译器生成和优化它们,使较小的工作单元“更容易并行化”来最小化并行计算原语的成本。)

我们也可以考虑讨论大O符号及其应用。我们都希望并行原语的成本为O(1)。如果是这种情况,那么如果您找到成本为O(x) > O(1)的工作,则该工作是并行化的好候选对象。如果您提出的工作也是O(1),则其有效性取决于常数因子,我们又回到了上面的校准问题。

如果没有足够大的单元收集工作,就会出现问题。代码移动、算法替换等都是实现此效果的有用方法。

最后,还有同步问题:我的并行单元何时必须交互,应该使用哪些原语,以及这些原语的成本如何?(比您预期的要高!)


6
我猜这取决于书籍/受众的基础或高级程度。当您第一次从单线程转向多线程编程时,通常会掉入巨大的坑(许多人永远无法摆脱,例如有关Control.Invoke的所有混乱问题)。 无论如何,为了增加一些与编程本身无关的想法,以及软件过程中其他相关任务:
- 测量:决定要改进的指标,正确测量它(很容易意外地测量错误事物),使用正确的工具,区分信号和噪声,解释结果并理解其原因。 - 测试:如何编写测试,使其容忍不重要的非确定性/交错,但仍然能够准确捕捉程序行为。 - 调试:工具、策略、什么情况下“难以调试”意味着反馈来改善您的代码/设计和更好地划分可变状态等等。 - 物理与逻辑线程亲和力:理解GUI线程,理解例如F#MailboxProcessor/代理如何封装可变状态并在多个线程上运行,但始终只有一个逻辑线程(一个程序计数器)。 - 模式(以及何时适用):fork-join、map-reduce、producer-consumer等。 我期望会有大量受众,例如“帮助,我的单线程应用程序的CPU利用率为12%,我想学习足够多只需少量工作即可使其运行速度提高4倍”,还有一小部分受众,例如“随着我们添加内核,我的应用程序的规模似乎是次线性的,因为这里似乎存在争用,是否有更好的方法使用?”因此,挑战的一部分可能是为每个受众提供服务。

关于测试,我的公司使用了微软的 CHESS 工具(http://research.microsoft.com/en-us/projects/chess/)来彻底测试我们多线程代码的所有交错,并且从一开始到结束都非常出色。这个工具在这里也可能很有用。 - Juliet

5

由于您已经写了一本关于在.Net中进行多核编程的书,我认为您可以稍微超越多核

例如,您可以使用一章节来讨论在.Net分布式系统中的并行计算。不过目前在.Net中还没有成熟的框架,DryadLinq是最接近的。(另一方面,Java平台中的Hadoop及其相关工具确实非常好用。)

您也可以使用一章节来演示一些GPU计算的内容。


4

有一件事让我感到困惑,那就是解决特定类型问题时应该采用哪种方法。有代理,有任务,异步计算,MPI 分布式计算等多种方法可供选择,但对于许多问题,我难以理解为什么应该优先选择其中的一种方法。


3
要理解:低级内存细节,如内存获取和释放语义之间的区别。
大多数其他概念和想法(任何东西都可以交错,竞争条件等)只需要一点使用就不那么困难了。
当然,实践起来尤其是如果某些东西有时会失败,是非常困难的,因为你需要在多个抽象层次上工作,以理解发生了什么,所以保持设计简单,并尽可能地设计出锁定等需求(例如,使用不可变数据和更高级别的抽象)。

2

人们经常试图从多个线程更新数据结构,发现太难了,然后有人插话说“使用不可变的数据结构!”,于是我们的持久编码器写下了以下内容:

不可变数据结构到底是什么?

这并不是那么理论上的细节,而更多的是实际实现的细节使人们感到困惑。

ImmutableSet set;

ThreadLoop1()
    foreach(Customer c in dataStore1)
        set = set.Add(ProcessCustomer(c));

ThreadLoop2()
    foreach(Customer c in dataStore2)
        set = set.Add(ProcessCustomer(c));

Coder一直以来都听说过不可变数据结构可以在不加锁的情况下进行更新,但新代码出现了明显的问题。

即使你的目标对象是学者和经验丰富的开发人员,对于不可变编程习惯的基础知识的简要介绍也有好处。

如何在线程之间分配大致相等的工作量?

正确地完成这一步是很难的。有时,您需要将一个单一的进程分成 10,000 个可以并行执行的步骤,但并非所有步骤所需的时间都相同。如果您将工作分配给 4 个线程,并且前三个线程在 1 秒钟内完成,而最后一个线程需要 60 秒,那么您的多线程程序与单线程版本基本相同,对吗?

那么,如何在所有线程之间分配大致相等的工作量?解决垃圾箱装填问题的许多良好试探方法应该也适用于此。

多少线程?

如果您的问题可以很好地并行化,则添加更多线程应该会使其更快,对吗?嗯,并非完全如此,在这里需要考虑很多东西:

即使是单核处理器,添加更多线程也可以使程序运行得更快,因为更多的线程提供了更多机会让操作系统调度您的线程,从而使它比单线程程序获得更多的执行时间。但是,随着收益递减的法则,添加更多线程会增加上下文切换的数量,因此,在某个点上,即使您的程序具有最长的执行时间,其性能仍可能比单线程版本差。

那么,如何启动足够多的线程才能使执行时间最小化?

如果有很多其他应用程序启动线程并竞争资源,那么如何检测性能变化并自动调整您的程序呢?


Jon是函数式编程的坚定支持者,这确实改变了哪些任务是困难的。我猜你错过了问题上的"f#"标签,他应该指出他的函数式方法。装箱可能是一种分区的方法,但工作窃取和工作队列,在这种情况下分区不是预先确定的,似乎在现实世界中更受欢迎。 - Ben Voigt
@Ben:请注意,我倡导像F#这样的不纯函数式语言,并且并不排斥变异和可变数据结构。实际上,在并行计算的情况下,我认为变异通常是必不可少的。 - J D

1

我觉得在复杂的模式下,同步数据在工作节点之间移动的概念非常难以可视化和编程。

通常我发现调试也很麻烦。


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