如何解释为什么多线程很难


84

我是一个相当不错的程序员,我的老板也是一个相当不错的程序员。尽管他似乎低估了诸如多线程之类的某些任务以及它的难度(我发现,除了运行几个线程,等待所有线程完成然后返回结果之外,其他事情非常困难)。

当您开始担心僵局和比赛条件时,我觉得这很困难,但老板似乎并不喜欢这一点-我认为他从未遇到过。只是打个锁就差不多了。

那么,我如何介绍他,或者解释为什么他可能低估了并发,并行和多线程的复杂性?还是我错了?

编辑:他做了些什么-遍历一个列表,为该列表中的每个项目创建一个线程,该线程根据该项目中的信息执行数据库更新命令。我不确定他如何控制一次执行多少个线程,我想如果运行太多(如果他使用信号量的话),他一定会将它们添加到队列中。


17
多线程很容易。正确的同步很难。
Vineet Reynolds,

33
带三个人进入房间,最好使用不同的口音,并让他们同时解释并发问题的不同重叠部分。
greyfade11年

多线程处理可能非常困难或非常容易,具体取决于当前的问题和语言支持。Clojure使操作变得容易clojure.org/concurrent_programming
Job

4
@Job并发编程总是很困难(在现实世界中的项目中),无论您使用哪种语言。当您要将Scala,Clojure或Erlang与使用并鼓励可变状态的语言进行比较时,会使它有些理智。
Chiron

4
我最喜欢的比喻是:“您会同时服用安眠药和泻药吗?” 即使使用复杂的消息队列,顺序也是正确完成并发的结果。除非您有丰富的经验,否则这对许多人来说很难
Tim Post

Answers:


29
  1. 如果您可以依靠任何数学经验,请说明本质上确定性的正常执行流程如何不仅变为不确定性(具有多个线程),而且呈指数级复杂化,因为您必须确保对机器指令的所有可能的交织仍然可以正确地完成任务。一个简单的丢失更新或脏读情况的例子通常令人大开眼界。

  2. “轻松锁定” 一个简单的解决方案... 如果您不关心性能,它可以解决您的所有问题。尝试举例说明,如果亚特兰大每当有人订购一本书时亚马逊不得不锁定整个东海岸,将会给性能带来多大的打击!


1
+1讨论数学复杂性-这就是我了解共享状态并发的困难的方式,也是我在提倡消息传递体系结构时通常会提出的论点。-1表示“对它拍上一个锁” ...该短语表示使用锁的一种思维方法,很可能会导致死锁或行为不一致(因为驻留在不同线程中的代码客户端会产生冲突)请求,但彼此之间不同步,则客户端将具有库状态的不兼容模型)。
艾丹·库利(Aidan Cully)

2
亚马逊确实必须在处理订单时短暂地将单个物品的库存锁定在一个仓库中。如果某个特定项目突然发生大量运转,则该项目的订购性能将受到影响,直到供应用尽并且对库存的访问变为只读(因此可以100%共享)。亚马逊追求的其他程序没有做到的一件事是能够在重新进货之前对订单进行排队,并且可以选择在将重新进货的库存用于新订单之前为排队的订单提供服务。
Blrfl 2011年

@Blrfl:如果程序被编写为使用通过队列传递消息,则可以做到这一点。不需要使所有消息都通过单个队列传递给特定线程…
Donal Fellows

4
@Donal研究员:如果一个仓库中有100万个小部件库存,并且100万个订单在同一时刻到达,则所有这些请求都将在某种程度上进行序列化,同时将物料与订单进行匹配,无论它们如何处理。实际的现实情况是,亚马逊库存中可能永远没有太多的小部件,以致于在库存用完之前,处理大量订单的等待时间变得令人无法接受,同时其他所有人都可以被告知(并行),“我们已经淘汰了。 ” 消息队列是防止死锁的好方法,但是它们不能解决资源有限的高争用问题。
Blrfl 2011年

79

多线程简单。对应用程序进行多线程编码非常非常容易。

有一个简单的技巧,这是使用一个精心设计的消息队列(也没有推出自己的),以线程之间传递数据。

困难的部分是尝试让多个线程以某种方式神奇地更新共享对象。那时候它容易出错,因为人们不注意当前的比赛条件。

许多人不使用消息队列,而是尝试更新共享对象并为自己创建问题。

设计一个在多个队列之间传递数据时运作良好的算法变得困难。那很难。但是(通过共享队列)共存线程的机制很容易。

另外,请注意线程共享 I / O资源。与I / O绑定的程序(例如,网络连接,文件操作或数据库操作)不太可能随着大量线程而更快地运行。

如果要说明共享库更新问题,那很简单。用一堆纸卡坐在桌子对面。写下一组简单的计算-4或6个简单公式-页面下方有很多空间。

这是游戏。你们每个人都读一个公式,写一个答案,并在答案上放一张卡片。

你们每个人都会做一半的工作,对不对?您完成了一半的时间,对不对?

如果您的老板不怎么想,只是刚开始,您将以某种方式结束冲突,并且都将答案写在相同的公式上。这没有用,因为你们两个人在写作之前阅读之间存在固有的竞争条件。没有什么可以阻止您阅读相同的公式并覆盖彼此的答案。

有很多方法可以使用不良或未锁定的资源来创建竞争条件。

如果要避免所有冲突,请将纸张切成一堆公式。您无需排队,写下答案,然后发布答案。没有冲突,因为你们俩都从一个仅读者的消息队列中进行读取。


即使将纸张切成一堆也不能完全解决问题-您仍然遇到这样的情况,您和您的老板会同时寻求一个新的公式,而您的指头也会碰到他。实际上,我想说这是最常见的线程问题的代表。真正的重大错误是尽早发现的。真正异常的错误永远存在,因为没有人可以复制它们。像这样的合理的比赛条件在测试中不断出现,最终所有(或更可能是大多数)都被淘汰了。
Airsource Ltd

@AirsourceLtd您到底在说什么“把拳头打进他的嘴里”?只要您有一个阻止两个不同线程接收同一条消息的消息队列,就不会有问题。除非我误会了你的意思。
扎克

25

多线程编程可能是并发最困难的解决方案。基本上,它只是对计算机实际功能的一个低级抽象。

有许多方法要容易得多,例如参与者模型(软件)事务存储。或使用不可变的数据结构(例如列表和树)。

通常,关注点的适当分离会使多线程更容易。当人们产生20个线程时,所有人都试图处理相同的缓冲区时,这就是通常会忘记的事情。在需要同步的地方使用反应堆,通常使用消息队列在不同的工作程序之间传递数据。
如果您锁定了应用程序逻辑,则说明您做错了什么。

所以是的,从技术上讲,多线程是困难的。
“对此加锁”几乎是解决并发问题的最小扩展方案,实际上使多线程的全部目的无效。它的作用是将问题恢复为非并行执行模型。您执行的次数越多,一次运行的线程(或死锁中的0)的可能性就越大。它打败了整个目标。
这就像在说“解决第三世界的问题很容易。只需在上面扔炸弹”。仅仅因为有一个简单的解决方案,这并不会使问题变得微不足道,因为您关心结果的质量。

但是在实践中,解决这些问题与其他任何编程问题一样困难,最好通过适当的抽象来解决。实际上,这很容易做到。


14

我认为这个问题有一个非技术性的角度-IMO是一个信任的问题。我们通常被要求复制复杂的应用程序,例如-哦,我不知道-例如Facebook。我得出的结论是,如果您不得不向未开始的人员/管理人员解释任务的复杂性,那么丹麦的情况就糟透了。

即使其他忍者程序员可以在5分钟内完成任务,您的估算仍基于您的个人能力。您的对话者应该学会相信您对此事的意见,或者雇用愿意接受其话的人。

挑战不是传递人们可能会忽略或无法通过对话掌握的技术含义,而是建立相互尊重的关系。


1
有趣的答案,尽管这是一个技术问题。但是,我确实同意您的意思……不过,在这种情况下,我的经理是一位非常出色的程序员,但是我只是认为,因为他没有遇到多线程应用程序的复杂性,所以他低估了它们。
Shoubs先生2011年

6

理解僵局的一个简单的思想实验就是“ 用餐哲学家 ”问题。我倾向于用来描述恶劣的比赛条件的例子之一是Therac 25的情况。

“只是轻描淡写地锁上”是没有碰到具有多线程的棘手错误的人的心态。而且可能认为您夸大了这种情况的严重性(我不是-可能会炸毁东西或杀死带有种族状况错误的人,尤其是最终嵌入汽车的嵌入式软件)。


1
也就是说,三明治问题:您要制作一堆三明治,但是只有一盘黄油和一把刀。通常,一切都很好,但最终有人会抓住黄油,而别人会抓住刀子..然后他们俩都站在那儿,等待对方放开他们的资源。
gbjbaanb

这样的僵局问题是否可以通过始终按固定顺序获取资源来解决?
compman 2011年

@compman,不。因为两个线程有​​可能在同一时刻尝试获取相同的资源,而这些线程不一定需要相同的资源集-只是重叠足以引起问题。一种方案是将资源“退回”,然后等待随机的一段时间,然后再重新获取它。退避期发生在许多协议中,最早的是Aloha。en.wikipedia.org/wiki/ALOHAnet
Tangurena 2011年

1
如果程序中的每个资源都有一个数字,并且当线程/进程需要一组资源时,它总是以递增的数字顺序锁定资源怎么办?我认为不会发生僵局。
compman 2011年

1
@compman:这确实是避免死锁的一种方法。可以设计一些工具来让您自动进行检查。因此,如果从未发现您的应用程序以递增的数字顺序锁定资源,那么您就永远不会有潜在的死锁。(请注意,只有当代码在客户的计算机上运行时,潜在的死锁才会变成真正的死锁)。
gnasher729

3

并发应用程序不是确定性的。由于程序员认为非常脆弱的总体代码量很少,因此您无法控制何时执行某个线程/进程的一部分相对于另一线程的任何部分执行。测试更加困难,需要更长的时间,并且不太可能找到所有与并发相关的缺陷。如果发现缺陷,则不能始终复制出细微的缺陷,因此很难修复。

因此,唯一正确的并发应用程序是可证明正确的应用程序,这在软件开发中并不经常实践。因此,S.Lot的答案是最好的一般建议,因为消息传递相对容易证明是正确的。


3

用两个词简短回答:可观察的不确定性

长答案:这取决于您遇到的问题使用哪种并行编程方法。在《计算机编程的概念,技术和模型》一书中,作者清楚地解释了编写并发程序的四种主要实用方法:

  • 顺序编程:没有并发性的基线方法;
  • 声明式并发:在没有可观察到的不确定性时可用;
  • 消息传递并发:大量实体之间传递的并发消息,其中每个实体在内部按顺序处理消息;
  • 共享状态并发:线程通过粗粒度的原子操作(例如锁,监视器和事务)更新共享的被动对象。

现在,除了明显的顺序编程之外,这四种方法中最简单的是声明式并发,因为使用这种方法编写的程序没有明显的不确定性。换句话说,没有竞赛条件,因为竞赛条件只是可观察到的不确定性行为。

但是缺乏可观察到的不确定性意味着存在一些我们无法使用声明式并发解决的问题。这是最后两种不太容易的方法起作用的地方。不太容易的部分是可观察到的不确定性的结果。现在它们都属于有状态并发模型,并且在表达能力上也等效。但是由于每个CPU内核数量的不断增加,最近业界似乎对消息传递并发越来越感兴趣,这可以从消息传递库(例如JVM的Akka)或编程语言(例如Erlang)的兴起中看出。

前面提到的Akka库(由理论上的Actor模型支持)简化了并发应用程序的构建,因为您不必再​​处理锁,监视器或事务。另一方面,它需要不同的方法来设计解决方案,即以某种方式思考如何对参与者进行分层组合。可以说这需要一种完全不同的思维方式,这最终可能比使用纯状态共享并发还要困难。

由于可观察到的不确定性,并发编程很困难,但是当对给定问题使用正确的方法以及支持该方法的正确的库时,可以避免很多问题。


0

我首先被告知,它可以通过看到一个简单的程序启动2个线程,并同时将它们同时从1-100打印到控制台,从而引发问题。代替:

1
1
2
2
3
3
...

您会得到更多这样的信息:

1
2
1
3
2
3
...

再次运行它,您可能会得到完全不同的结果。

我们大多数人已经接受过训练,可以假定我们的代码将按顺序执行。对于大多数多线程,我们不能认为这是“开箱即用”的。


-3

尝试使用多个锤子一次将一堆紧密隔开的钉子捣成糊状,而握住锤子的人之间没有任何联系……(假设它们被蒙住了)。

将其升级为盖房子。

试着晚上睡觉,想像你是建筑师。:)


-3

简单的部分:使用具有框架,操作系统和硬件的现代特征的多线程,例如信号量,队列,互锁的计数器,原子盒装类型等。

困难的部分:首先不使用任何功能来实现自己的功能,可能只是少数极有限的硬件功能,仅依靠跨多个内核的时钟一致性保证。


3
困难的部分确实更难,但即使是简单的部分也不是那么容易。
PeterAllenWebb 2011年
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.