我是否应该不再使用不赞成使用的多线程和多处理器编程实践?


36

在FORTRAN和BASIC的早期,基本上所有程序都是使用GOTO语句编写的。结果是意大利面条代码,解决方案是结构化编程。

同样,指针可能很难控制我们程序中的特征。C ++从大量的指针开始,但是建议使用引用。像STL这样的库可以减少我们的依赖。还有一些习惯用法来创建具有更好特性的智能指针,并且某些版本的C ++允许引用和托管代码。

诸如继承和多态性之类的编程实践在幕后使用了许多指针(就像结构化编程生成充满分支指令的代码一样)。像Java这样的语言消除了指针,并使用垃圾回收来管理动态分配的数据,而不是依赖程序员来匹配其所有new和delete语句。

在我的阅读中,我看到了似乎不使用信号量的多进程和多线程编程的示例。他们是使用同一名称使用不同的名称还是使用新的方法来保护资源并发使用?

例如,使用多核处理器进行多线程编程的系统的特定示例是OpenMP。它代表以下一个关键区域,不使用信号量,该信号量似乎不包含在环境中。

th_id = omp_get_thread_num();
#pragma omp critical
{
  cout << "Hello World from thread " << th_id << '\n';
}

本示例摘自:http : //en.wikipedia.org/wiki/OpenMP

或者,使用带有功能wait()和signal()的信号量对线程进行类似的保护,如下所示:

wait(sem);
th_id = get_thread_num();
cout << "Hello World from thread " << th_id << '\n';
signal(sem);

在此示例中,事情非常简单,只需简单的回顾就足以显示wait()和signal()调用是匹配的,即使有很多并发,也可以提供线程安全性。但是其他算法更复杂,并且使用多个信号量(二进制和计数),这些信号量分布在具有复杂条件的多个函数中,可以被许多线程调用。创建死锁或无法使线程安全的后果很难管理。

这些系统(例如OpenMP)是否消除了信号量的问题?
他们是否将问题转移到其他地方?
如何使用算法将自己喜欢的信号量转换为不再使用信号量?


你到底在说什么 你看见什么了?
svick

4
这并不意味着无礼,但是您可以删除前三段。他们并不是真的在乎您的问题,他们会超出他们的结论,只会引起很多争论。
dbracey 2012年

1
哇,大编辑。我已经回答了一个问题。这个问题仍然会在GOTO,指针,继承和多态性中徘徊,但是在我的回答中,我将这些问题搁置一旁,并集中在“过时的实践”问题上。
斯图尔特·马克

Answers:


15

是否存在不再应该使用的并发编程技术和实践?我会说是的

在当今看来,一种罕见的早期并发编程技术是中断驱动编程。这就是UNIX在1970年代的工作方式。请参阅有关UNIX的Lions 评论或Bach的UNIX操作系统设计。简而言之,该技术是在处理数据结构时临时挂起中断,然后再恢复中断。在BSD SPL(9)手册页有这种编码风格的例子。请注意,中断是面向硬件的,并且代码体现了硬件中断的种类与与该硬件相关联的数据结构之间的隐式关系。例如,操纵磁盘I / O缓冲区的代码在使用这些缓冲区时需要暂停来自磁盘控制器硬件的中断。

单处理器硬件上的操作系统采用了这种编程风格。对于应用程序而言,处理中断要少得多。一些操作系统有软件中断,我认为人们试图在它们之上构建线程或协程系统,但这并不是很普遍。(当然不是在UNIX世界中。)我怀疑今天的中断样式编程仅限于小型嵌入式系统或实时系统。

信号量是优于中断的,因为它们是软件结构(与硬件无关),它们提供了硬件设施上的抽象,并且可以启用多线程和多处理。主要问题是它们是非结构化的。程序员负责在整个程序中全局维护每个信号量与其保护的数据结构之间的关系。因此,我认为今天很少使用裸信号量。

向前迈出的另一小步是监视器,它封装了并发控制机制(锁和条件)以及受保护的数据。它被带入Mesa (备用链接)系统,并从那里带入Java。(如果您阅读了这篇Mesa论文,您会发现Java的监视器锁定和条件几乎是从Mesa完全复制过来的。)监视器非常有用,因为足够谨慎和勤奋的程序员可以仅使用关于代码和数据的本地推理来安全地编写并发程序。在显示器内。

还有其他库构造,例如Java java.util.concurrent软件包中的库构造,其中包括各种高度并发的数据结构和线程池构造。这些可以与其他技术结合使用,例如线程限制和有效的不变性。看到 Goetz等人的《Java Concurrency In Practice》。等 有待进一步讨论。不幸的是,当许多程序员真正应该只使用诸如ConcurrentHashMap之类的东西时,他们仍然使用锁和条件来滚动自己的数据结构,而库作者已经完成了繁重的工作。

上面的所有内容都有一些重要的特征:它们具有多个控制线程,可以在全局共享的可变状态下交互。问题在于,以这种方式进行编程仍然很容易出错。一个小错误很容易被忽视,从而导致难以重现和诊断的不良行为。可能没有程序员会“足够谨慎和勤奋”地以这种方式开发大型系统。至少很少。因此,我要说,如果可能的话,应避免使用具有共享的可变状态的多线程编程。

不幸的是,目前尚不清楚是否在所有情况下都可以避免。许多编程仍以这种方式进行。很高兴看到它被其他东西取代了。从答案贾罗德·罗伯逊 davidk01的指出了诸如不变数据,函数式编程,STM和消息传递之类的技术。有很多值得推荐的东西,并且都在积极开发中。但是我认为它们还没有完全取代好的老式共享可变状态。

编辑:这是我对特定问题的最后回应。

我对OpenMP不太了解。我的印象是,它对于高度并行的问题(例如数值模拟)非常有效。但这似乎不是通用的。信号量构造看起来很底层,并且要求程序员维护信号量和共享数据结构之间的关系,而上面我已经描述了所有问题。

如果您有使用信号量的并行算法,那么我不知道有任何通用技术可以对其进行转换。您也许可以将其重构为对象,然后围绕它构建一些抽象。但是,如果您想使用诸如消息传递之类的方法,我认为您确实需要重新构思整个问题。


谢谢,这是很棒的信息。我将仔细阅读这些参考资料,并深入探讨您提到的对我来说是新的概念。
DeveloperDon

为java.util.concurrent +1并同意注释-从1.5开始,它就已经存在于JDK中,我很少看到它使用过。
MebAlone

1
我希望您强调一下,已经存在的结构不滚动自己的结构是多么重要。如此之多,如此之多的错误……
corsiKa 2014年

我认为说“信号量是优于中断的进步,因为它们是软件结构(与硬件无关) ”,这是不正确的。信号量取决于CPU来实现Compare-and-Swap指令,或者它是多核变量
乔什·皮尔斯

@JoshPearce当然信号量是实现使用硬件构造,但它们是一个抽象的是独立于任何特定的硬件结构,例如CAS,检查并设置,cmpxchng等
斯图尔特标记

28

回答问题

普遍的共识是,共享的可变状态是Bad™,而不变的状态是Good™,这被功能性语言和命令性语言一次又一次地证明是正确和正确的。

问题在于主流命令式语言并非仅设计用于处理这种工作方式,这些语言不会在一夜之间发生变化。这是与之比较GOTO存在缺陷的地方。不变的状态和消息传递是一个很好的解决方案,但它也不是万能药。

有缺陷的前提

这个问题是基于对有缺陷的前提的比较。这GOTO是实际的问题,并且由Intergalatic语言设计者通用委员会和软件工程联盟 © 弃用了!没有GOTO机制,ASM将根本无法工作。相同的前提是,原始指针是C或C ++的问题,而某些智能指针是灵丹妙药,而事实并非如此。

GOTO不是问题,程序员是问题。这同样适用于共享可变状态。它本身不是问题,是程序员使用它才是问题。如果有一种方法可以生成使用共享可变状态的代码,而这种方法从来没有任何竞争条件或错误,那么这将不是问题。很像,如果您从不编写具有GOTO相同结构的意大利面条式代码,这也不是问题。

教育是灵丹妙药

白痴的程序员是什么人deprecated,每一个流行的语言仍然有GOTO直接或间接的构建,这是一个best practice正确使用具有此类型构造的每一种语言。

示例: Java具有标签,并且try/catch/finally两者都可以直接用作GOTO语句。

我与之交谈的大多数Java程序员甚至都不知道在他们眼中像僵尸一样immutable重复在外the String class is immutable面对他们意味着什么。他们肯定不知道如何final正确使用关键字来创建immutable类。因此,我很确定他们不知道为什么使用不可变消息传递的消息如此之大,为什么共享可变状态如此之差。


3
+1好答案,写清楚并指出了可变状态的基本模式。IUBLDSEU应该成为一个模因:)
Dibbeke 2012年

2
GOTO是“请,真的,请不要在这里发动一场火焰大战,我要双重犬敢”的代名词。这个问题使人们望而却步,但并没有给出一个很好的答案。很好地提到了函数式编程和不变性,但是这些陈述没有实质意义。
Evan Plaice 2012年

1
这似乎是一个矛盾的答案。首先,您说“ A是不好的,B是好的”,然后说“不赞成使用白痴”。第一段是否适用相同的内容?我不能只听完您回答的最后一部分,然后说“在每种语言中正确使用共享的可变状态是一种最佳做法”。另外,“证明”是一个非常有力的词。除非你有你不应该使用它真正有力的证据。
luiscubal 2012年

2
我无意发动火焰战争。在Jarrod对我的评论做出回应之前,一直认为GOTO不会引起争议,并且可以比喻地很好地工作。当我写这个问题时,我并没有想到这个问题,但是Dijkstra在GOTO和信号量上都为零。Edsger Dijkstra在我看来似乎是一个巨人,并因信号量(1965年)和早期(1968年)关于GOTO的学术著作的发明而著称。迪克斯特拉的倡导方法常常是硬朗和对抗的。争议/对抗对他很有用,但我只想要有关信号灯替代方案的想法。
DeveloperDon

1
在现实世界中,许多程序应该用来建模可变的东西。如果在5:37 am,对象#451在那一刻(5:37 am)保存了现实世界中某物的状态,而随后现实世界中的物态发生了变化,则可能是该对象的身份代表了真实事物的状态是不可变的(即,该事物将始终由对象#451表示),或者对象#451的状态是不可变的,但不能同时存在。在许多情况下,使身份不变是比使对象#451不可变更为有用。
2012年

27

在学术界,最新的流行似乎是软件事务存储(STM),它有望通过使用足够智能的编译器技术,将多线程编程的所有繁琐细节从程序员手中夺走。在幕后仍然是锁和信号灯,但是作为程序员的您不必担心。这种方法的好处还不清楚,也没有明显的竞争者。

Erlang使用消息传递和代理进行并发,这是比STM更简单的模型。通过消息传递,您绝对不必担心任何锁和信号灯,因为每个代理都在自己的小型Universe中运行,因此没有与数据相关的竞争条件。您仍然有一些奇怪的情况,但是它们比活锁和死锁复杂得多。JVM语言可以利用Akka并获得消息传递和actor的所有好处,但是与Erlang不同,JVM没有对actor的内置支持,因此,最终Akka仍使用线程和锁,但您却可以使用程序员不必担心。

我知道的另一个不使用锁和线程的模型是使用期货,这实际上只是异步编程的另一种形式。

我不确定C ++中有多少这种技术可用,但是如果您看到的是没有明确使用线程和锁的东西,那么这将是上述用于管理并发性的技术之一。


+1为新术语“有毛细节”。大声笑的人。我只是不停地笑这个新学期。我想从现在开始我将使用“有毛代码”。
Saeed Neamati 2012年

1
@Saeed:我以前听过这种表达,这并不少见。我同意这很有趣:-)
卡梅伦(Cameron)

1
好答案。据说.NET CLI也支持信令(而不是锁定),但是我还没有遇到一个完全取代锁定的示例。我不确定异步是否重要。如果您在谈论Javascript / NodeJ之类的平台,它们实际上是单线程的,并且仅在较高的IO负载下才更好,因为它们不容易受到资源最大化的限制(例如,在大量丢弃的上下文中)。在CPU密集型负载上,使用异步编程几乎没有/没有好处。
伊万·普赖斯

1
有趣的答案是,我以前从未遇到过期货。另请注意,在诸如Erlang的消息传递系统中,您仍然可能存在死锁活动锁CSP允许您对死锁活动锁进行正式的推理,但是它并不能单独阻止死锁活动锁
Mark Booth

1
我将添加“无锁”并等待该列表的免费数据结构。
stonemetal 2012年

3

我认为这主要是关于抽象级别。通常在编程中,以更安全或更易读的方式抽象出一些细节很有用。

这适用于控制结构:ifs,fors和甚至try- catch块只是gotos的抽象。这些抽象几乎总是有用的,因为它们使您的代码更具可读性。但是在某些情况下您仍然需要使用goto(例如,如果您正在手工编写程序集)。

这也适用于内存管理:C ++智能指针和GC是对原始指针和手动内存取消/分配的抽象。有时,这些抽象是不合适的,例如,当您确实需要最大性能时。

同样适用于多线程:诸如Future和actor之类的东西仅仅是对线程,信号量,互斥量和CAS指令的抽象。这样的抽象可以帮助您提高代码的可读性,还可以避免错误。但是有时候,它们根本不合适。

您应该知道可用的工具以及它们的优缺点。然后,您可以为任务选择正确的抽象(如果有)。较高的抽象级别不会降低较低的级别,在某些情况下,总是存在抽象不合适的情况,最好的选择是使用“旧方法”。


谢谢,您正在抓住一个类比,关于WRT信号量的答案是否已被弃用,我没有一个先入为主的想法,甚至没有斧头。对我来说,最大的问题是在系统中似乎没有更好的方法和信号灯没有丢失一些重要的东西,并且他们将无法完成所有的多线程算法。
DeveloperDon

2

是的,但是您不太可能碰到其中一些。

在过去,使用阻塞方法(屏障同步)很常见,因为编写良好的互斥锁很难做到。您仍然可以在最近的事物中看到这种痕迹。使用现代并发库为您提供了更丰富,经过充分测试的并行化和进程间协调工具集。

同样,一种较旧的做法是编写复杂的代码,以便您可以弄清楚如何手动对其进行并行化。这种形式的优化(如果您弄错了,可能会造成危害)的优化在很大程度上已经消失了,因为为您执行此操作的编译器的出现,必要时展开循环,可预测地跟随分支等,但这并不是新技术。 ,在市场上至少存在15年。利用线程池之类的东西还可以规避一些过去非常棘手的代码。

因此,也许不推荐使用的做法是自己编写并发代码,而不是使用经过良好测试的现代库。


谢谢。似乎有很大的潜力可以使用并发编程,但是如果不按规范使用它的话,它可能是潘多拉盒子。
DeveloperDon

2

苹果的Grand Central Dispatch是一种优雅的抽象,它改变了我对并发的想法。根据我的拙劣经验,它对队列的关注使异步逻辑的实现变得简单了一个数量级。

当我在可用的环境中进行编程时,它已取代了我对线程,锁和线程间通信的大多数使用方式。


1

并行编程的主要变化之一是CPU的速度比以前快得多,但是要实现该性能,需要一个填充良好的缓存。如果您尝试同时运行多个线程,并在它们之间连续交换,则几乎总是会使每个线程的缓存失效(即,每个线程需要不同的数据进行操作),最终导致的性能损失比您大得多。习惯于较慢的CPU。

这就是为什么异步或基于任务(例如Grand Central Dispatch或Intel的TBB)框架更受欢迎的原因之一,它们一次运行代码1任务,在移至下一个任务之前完成该任务-但是,您必须对每个任务进行编码除非您想弄乱设计,否则每个任务只需花费很少的时间(即,并行任务实际上已经排队)。CPU密集型任务将传递到备用CPU内核,而不是在处理所有任务的单线程上处理。如果也没有真正的多线程处理,它也更易于管理。


太棒了,感谢您对Apple和I​​ntel技术的引用。您的答案是否指出了管理线程与核心关联的挑战?由于多核处理器可能会在每个核心上重复L1缓存,因此缓解了某些缓存性能问题。例如:software.intel.com/en-us/articles/… 具有四个高速缓存命中率的四个内核的高速高速缓存的速度可以是相同数据上具有多个高速缓存未命中的一个内核的4倍以上。矩阵乘法即可。不能在4个内核上随机调度32个线程。让我们使用亲和力获得32个核心。
DeveloperDon

尽管确实存在相同的问题,但并非完全相同-核心相似性只是指一个任务在核心之间反弹的问题。如果任务被中断,替换为新任务,则原来的问题相同,那么原始任务将在同一核心上继续。英特尔的说法是:与内核数量无关,高速缓存命中=快速,高速缓存未命中=慢。我认为他们正在试图说服您购买他们的芯片,而不是AMD :)
gbjbaanb 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.