我正在努力用简单的话来解释线程中的“死锁”,所以请帮忙。什么可能是“死锁”(例如,在 Java 中)的最佳示例,它是如何分步发生的以及如何防止它?但没有深入细节。我知道这就像问两个相反的事情,但仍然如此。如果你之前有任何并发编程培训经验——那就太棒了!
15 回答
杰克和吉尔碰巧想同时做三明治。两人都需要一片面包,所以他们都去拿面包和一把刀。
杰克先拿到刀,而吉尔先拿到面包。现在杰克试图找到一条面包,吉尔试图找到刀,但两人都发现他们完成任务所需的东西已经在使用中。如果他们都决定等到他们需要的东西不再使用,他们将永远等待对方。僵局。
最简单的方法是让两个不同的线程尝试以不同的顺序获取两个锁:
thread 1:
lock(a)
lock(b)
thread2:
lock(b)
lock(a)
假设线程 1 获得锁 A,然后进入睡眠状态。线程 2 获取锁 B,然后尝试获取锁 A;由于锁定 A,线程 2 将进入睡眠状态,直到线程 A 被解锁。现在线程 1 重新唤醒并尝试获取锁 B 并将进入睡眠状态。
对于这种情况,有几种方法可以防止它:
- 一个线程永远不需要同时持有两个锁。
- 如果必须同时持有两个锁,则必须始终以相同的顺序获取它们(因此在我上面的示例中,需要修改线程 2 以在请求锁 B 之前请求锁 A)。
Thrd 1 --- Lock A - atmpt lock on B -
\ / \
\ / \
\ / \
--- Lock A / --- wait for lock on B
Thrd 2--- Lock B - atmpt lock on A -
\ / \
\ / \
\ / \
--- Lock B / --- wait for lock on A
线程 1 运行,锁定 A,做一些事情,并被锁定 B 的线程 2 中断,做一些事情并被尝试锁定 B 的线程 1 中断,但线程 2 已锁定 B,因此线程 1 等待并被中断由线程 2 尝试锁定 A,但线程 1 已锁定 A,因此线程 2 必须等待。
两个线程都在等待另一个线程释放他们试图获得锁的资源上的锁......
僵局
我宁愿用与计算机完全无关的术语来解释它,因为这通常是传达想法的最佳方式。
我有一个五岁的儿子和一个三岁的女儿。两人都想做同样的涂色书。
女儿抓铅笔,儿子抓书。在得到对方之前,双方都不会放弃他们所拥有的。
那是僵局。没有比这更简单的了。
您的进程(或子进程)相互等待,并将继续无限期地等待,直到其他高级进程(如爸爸)进入并打破僵局。
至少对于孩子来说,你可以(有时)让其中一个看到原因并放弃他们的锁。这在计算机上通常是不可能的,因为进程除了等待该资源之外什么都不做(尽管有时孩子也会进入这种状态)。
遵循一条规则将保证不会发生死锁:
- 让所有执行线程以相同的顺序分配资源。
遵循一些额外的规则将使您的线程不太可能减慢彼此的速度,但请记住,上述规则应优先于所有其他规则:
- 仅在需要时分配资源。
- 完成后立即释放它们。
演示死锁的另一个好方法是使用 SQL Server。
使用具有不同隔离级别的事务,您可以演示一个事务如何无限期地等待被另一个事务锁定的表。
这里的优点是您可以使用 SQL Management Studio 进行演示。过去,我在教授“SQL Server 简介”级别的培训课程时,曾用它来向人们解释死锁。
大多数参与者对这个理论有疑问,但是当他们看到它在行动时,这一切(通常)都会变得清晰。
简而言之:事务 A(尚未完成)采用显式表锁。第二个事务 B 尝试从事务 A 锁定的表中读取。事务 B 处于死锁状态,直到事务 A 提交或回滚。
您可以通过创建两个单独的线程来相当容易地在代码中解释这一点,这两个线程依次创建事务。希望能帮助到你。
通常并发编程的类通过例子来解释死锁。我认为餐饮哲学家的问题将是一个很好的例子。您可以使用 Java 开发此示例并解释当两个哲学家拿着左叉并等待右叉时发生死锁。(或相反亦然)。
使用这个在 Java 上实现的示例,我从并发编程中学到了很多概念。
死锁是当两个线程相互等待时,在另一个线程先完成之前,两个线程都无法继续,因此两个线程都被卡住了。
死锁至少需要 2 个锁,并且两个线程都必须包含获取锁并等待锁被释放的代码。
线程 1 有锁 A 并且想要锁 B,所以它等待锁 B 被释放。
线程 2 有锁 B 并且想要锁 A,所以它等待锁 A 被释放。
现在你有一个僵局。两个线程都在等待一个锁,所以两个线程都没有执行,所以两个线程都不能释放另一个正在等待的锁。
当您有 2 个不同的资源需要 2 个不同的线程锁定才能使用它们时,就会发生死锁。线程以相反的顺序锁定它们,因此无法继续执行,直到其中一个线程退出。
维基百科有几个很好的现实生活中的死锁例子。
当一个工作人员被另一个工作人员阻止时,就会发生锁链。因为B,A不能继续。链条可以更长:A被B挡住,B被C挡住,C被D挡住。
死锁是锁链形成循环的时候。A 被 B 堵住,B 被 C 堵住,C 被 A 堵住,链条已经形成了一个循环,不可能有进展。
防止死锁的典型方法是使用锁层次结构:如果每个工作人员总是以相同的顺序获取锁,那么死锁是不可能的,因为每个阻塞都发生在一个工作人员之间,而不是持有排名 X 的锁并等待排名 Y 的资源,其中X > Y 总是。在这种情况下无法形成循环,因为它需要至少一名工作人员违反层次结构才能关闭循环。至少理论是这样的。在实践中,很难提出现实的层次结构(不,资源地址不起作用)。
如果无法避免死锁(例如数据库系统),那么解决方案是让专用线程检查死锁链以寻找循环并杀死其中一个参与者以释放循环。
(稍微过度简化)有两个人,将螺母拧到螺栓上。
程序(两者相同)是:
- 拿起一个螺母或螺栓
- 拿起一个螺栓或一个螺母(无论你还没有)
- 将螺母拧到螺栓上
- 将完成的组件放入“完成”堆中。
- 如果螺母和螺栓仍然存在,请转到步骤 1
那么当只剩下一个螺母和一个螺栓时会发生什么?第一个人拿起一个螺母,第二个人抓住一个螺栓。到目前为止一切顺利,但现在他们被困住了,每个人都有其他人需要的资源。
如果没有特别的指示,他们将永远僵在那里。
或者你可以给他们看这个视频
就餐哲学家——你有 4 个人坐在一张桌子旁,还有 4 支筷子。你需要两根筷子才能吃。想象一下每个哲学家都尝试吃如下:
- 拿起左筷子。
- 拿起右边的筷子。
- 吃。
- 把右筷子放回去。
- 把左筷子放回去。
每个人都做第 1 步。现在第 2 步是不可能的,因为每个人都在等待右边的人放弃左边的人,而他们不会这样做。这是僵局。如果他们只是轮流,那么他们都可以吃东西,但他们都饿死了。
Guffa的描述很好。
我发现避免死锁的最佳方法是仅锁定您私有的资源,并在调用您没有独占控制权的任何内容之前释放锁定。
唯一的问题是,这可能需要您从使用锁来保持一致性转变为使用补偿操作,但从长远来看,它可能不太可能导致问题,无论如何。
这篇文章很适合阅读这个问题。
想象一下,你和你的女朋友为了离开家应该开门而吵架。道歉的人会开门。她在等你道歉,你在等她道歉,这导致这对夫妇永远不会离开家,因为他们都拒绝道歉。
想象一个罪犯劫持人质并要求赎金。你带着一个装满钱的手提箱出现了。
罪犯在拿到钱之前永远不会释放人质。在你得到人质之前,你永远不会释放这些钱。僵局。
这里的类比是:
- 你和罪犯是主线
- 装满钱的手提箱和人质是资源
样品1:
我答应过从不做承诺
样品2:
与灯神对话:我的愿望是你永远不许愿望成真
样品3:
招聘人员:如果你解释僵局,我会雇用你。
候选人:如果你雇用我,我会解释什么是死锁