死锁是什么?

200

在编写多线程应用程序时,最常遇到的问题之一是死锁。

我向社区提出的问题是:

  1. 什么是死锁?

  2. 如何检测它们?

  3. 您如何处理它们?

  4. 最后,您如何防止它们发生?


55
亲爱的,你先请。 - Voicu
19个回答

253

锁定指的是多个进程同时尝试访问同一资源时发生的情况。

一个进程失败并必须等待另一个进程完成。

死锁发生在等待的进程仍然持有另一个进程需要才能完成的资源上。

所以,举个例子:

进程 X 和进程 Y 使用资源 A 和资源 B

  • X 开始使用 A。
  • X 和 Y 尝试开始使用 B
  • Y 获胜并首先获得 B
  • 现在 Y 需要使用 A
  • A 被 X 锁定,而 X 正在等待 Y

避免死锁的最佳方法是尽量减少进程交叉使用资源的情况。尽可能减少锁定任何东西的需求。

在数据库中,避免在单个事务中对不同的表进行大量更改,避免触发器,并尽可能切换到乐观/脏/无锁读取。


12
这里我使用“进程”作为一般概念,而非特指操作系统进程。这些可能是线程,也可能是完全不同的应用程序或数据库连接。模式是相同的。 - Keith
1
你好,假设有这样的情况:线程A锁定资源A并且有一个长时间的进程。线程B正在等待锁定资源A。CPU时间使用率为20%,你认为这可以被视为死锁情况吗? - rickyProgrammer
2
@rickyProgrammer 不是死锁,只是普通的锁等待,尽管它们之间的区别有点学术性。B 等待慢的 A 是锁等待,B 等待 A 然后 A 又在等待 B 是死锁。 - Keith
2
@rickyProgrammer,由于循环队列的原因,这是一个无论你等多久都不会释放的锁。 - Keith
@Keith 哇,我明白了,所以我试图重新表述一下给定的情况,使它成为死锁情况:线程A锁定资源A并等待锁定资源B。线程B锁定资源B并等待锁定资源A。我相信现在这就是一个死锁了? - rickyProgrammer
显示剩余10条评论

155

让我举一个现实(实际上并不真实)的例子来解释死锁情况,这个例子来自于犯罪电影。想象一下,一个罪犯挟持了人质,而对此,一名警察也挟持了罪犯的朋友作为人质。在这种情况下,如果警察不放走罪犯的朋友,罪犯是不会放走人质的。同样地,除非罪犯释放人质,警察也不会放走罪犯的朋友。这是一种无休止的不信任局面,因为双方都坚持对方先采取行动。

罪犯和警察的场景

enter image description here

简单来说,当两个线程需要两个不同的资源,并且它们中的每一个都拥有另一个需要的资源的锁时,就会发生死锁。

另一个关于死锁的高级解释:破碎的心

你和一个女孩约会,在一次争吵之后,双方对彼此心生怨恨,等待对方的“我很抱歉,我想念你”的电话。在这种情况下,双方都希望只有当其中一方收到了对方的“我很抱歉”电话时才能互相沟通。因为双方都不会主动开始沟通,而是被动地等待着对方开始沟通,最终陷入了死锁状态。


线程不应该属于不同的进程吗?属于同一进程的线程也可能导致死锁吗? - lordvcs
1
@diabolicfreak,线程是否属于同一进程并不重要。 - Sam Malayek
4
现实生活中的另一个例子是四辆汽车同时来到两条等长道路相交的路口,每个人都需要给右侧的车让路,所以没有人可以继续前进。 - LoBo
1
这些现实生活的例子非常具体且有趣。 - Soup Endless
另一个“现实生活”例子:哲学家就餐问题 - Johan Råde
线程#1需要资源#2,但是犯罪分子拥有锁定。而“资源#2,锁定的所有者是警察”似乎标题混淆了。 - bravmi

37

只有当您有两个或多个可以同时获取的锁并且它们以不同的顺序被抓取时,死锁才会发生。

避免死锁的方法包括:

  • 尽可能避免使用锁,
  • 避免使用多个锁
  • 始终按相同顺序获取锁。

避免死锁的第三个要点(始终按相同的顺序获取锁)非常重要,在编程实践中很容易被忘记。 - Qiang Xu

24

首先,我要定义进程来解释死锁。

进程进程实际上就是正在执行的程序

资源进程执行程序需要一些资源。资源类别可能包括内存、打印机、CPU、打开的文件、磁带驱动器、CD-ROM等。

死锁:当两个或多个进程持有某些资源并尝试获取更多资源时,它们无法释放资源直到完成其执行,这种情况或条件称为死锁。

死锁条件或情况

enter image description here

在上图中,有两个进程P1p2,还有两个资源R1R2

资源R1被分配给进程P1,资源R2被分配给进程p2。为了完成进程P1的执行,需要资源R2,因此P1请求R2,但是R2已经分配给了P2

同样地,进程P2需要资源R1才能完成其执行,但是R1已经分配给了P1

两个进程都无法释放其资源,直到它们完成执行为止。因此,它们都在等待另一个资源,将永远等待下去。所以这是一个死锁条件。

要发生死锁,必须满足四个条件。

  1. 互斥 - 每个资源都要么分配给恰好一个进程,要么可用。(两个进程不能同时控制相同的资源或处于其临界区段)。
  • 持有并等待 - 当前持有资源的进程可以请求新资源。
  • 无抢占 - 一旦进程持有资源,其他进程或内核无法将其夺走。
  • 循环等待 - 每个进程都在等待获取由另一个进程持有的资源。
  • 并且上述图表中所有这些条件都得到满足。


    11

    死锁是指线程在等待从未发生的事情。

    通常,当线程等待前一个所有者从未释放的互斥锁或信号量时,就会发生死锁。

    当涉及到两个线程和两个锁的情况时,也经常会发生死锁,如下所示:

    Thread 1               Thread 2
    
    Lock1->Lock();         Lock2->Lock();
    WaitForLock2();        WaitForLock1();   <-- Oops!
    

    通常情况下,您会检测到它们,因为您期望发生的事情从未发生过,或者应用程序完全挂起。


    1
    死锁发生在一个线程正在等待一些不可能发生的事情。 - user207421

    5
    您可以查看这个精彩文章,在死锁章节下。虽然是用C#编写的,但对于其他平台来说,思路仍然相同。以下是引用方便阅读:

    当两个线程都等待彼此持有的资源时,就会发生死锁,因此两个线程都无法继续进行。 最容易用两个锁来说明:

    object locker1 = new object();
    object locker2 = new object();
    
    new Thread (() => {
                        lock (locker1)
                        {
                          Thread.Sleep (1000);
                          lock (locker2);      // Deadlock
                        }
                      }).Start();
    lock (locker2)
    {
      Thread.Sleep (1000);
      lock (locker1);                          // Deadlock
    }
    

    4
    死锁是操作系统中多进程/多程序问题中的常见问题。 假设有两个进程P1,P2和两个全局共享资源R1,R2,在关键部分需要访问这两个资源。
    最初,操作系统将R1分配给进程P1,将R2分配给进程P2。 由于两个进程同时运行,它们可能开始执行它们的代码,但当一个进程进入关键部分时,问题就出现了。 因此,进程R1将等待进程P2释放R2,反之亦然...... 所以它们将永远等待(死锁状态)。
    一个小比喻...
    你的母亲(操作系统), 你(P1), 你的兄弟(P2), 苹果(R1), 刀(R2), 关键部分(用刀切苹果)。
    一开始,你的母亲把苹果和刀都给了你的兄弟。 两个人都很高兴并且在玩(执行他们的代码)。 你和你的兄弟在某个时候都想切苹果(关键部分)。 你不想把苹果给你的兄弟。 你的兄弟也不想把刀给你。 所以你们两个要等很久很久:)

    2

    基于锁的并发控制

    使用锁来控制共享资源的访问容易出现死锁,而事务调度器本身无法防止其发生。

    例如,关系型数据库系统使用各种锁来保证事务的 ACID 特性。

    无论使用哪个关系型数据库系统,在修改(如UPDATEDELETE)某个表记录时,都会始终获取锁。如果不锁定当前运行事务修改的行,则会破坏 原子性)。

    什么是死锁

    当两个并发事务由于彼此等待对方释放锁而无法进展时,即发生死锁,如下图所示。

    Deadlock

    因为两个事务都处于锁获取阶段,所以在获取下一个锁之前,它们都不会释放锁。

    从死锁中恢复

    如果你正在使用依赖锁的并发控制算法,那么就存在遇到死锁的风险。死锁可以在任何并发环境中发生,不仅仅是在数据库系统中。

    例如,如果两个或多个线程等待先前获取的锁,则多线程程序可能会发生死锁。如果这种情况发生在Java应用程序中,则JVM不能强制停止线程的执行并释放其锁。

    即使Thread类公开了一个stop方法,但该方法已自Java 1.1起被弃用,因为它可能导致对象在线程停止后处于不一致的状态。相反,Java定义了一个interrupt方法,作为提示,被中断的线程可以简单地忽略中断并继续其执行。

    因此,Java应用程序无法从死锁情况中恢复,而是由应用程序开发人员负责按照特定的锁获取请求顺序,以确保死锁永远不会发生。

    然而,数据库系统无法强制执行给定的锁获取顺序,因为不可能预见某个事务将要进一步获取哪些锁。保持锁顺序成为数据访问层的责任,数据库只能协助从死锁情况中恢复。

    数据库引擎运行一个单独的进程来扫描当前的冲突图以查找锁等待循环(由死锁引起)。 当检测到循环时,数据库引擎选择一个事务并将其中止,导致其锁被释放,以便其他事务可以继续进行。

    与JVM不同,数据库事务被设计为原子工作单元。因此,回滚会使数据库保持一致的状态。


    2

    当存在一组循环链的线程或进程时,每个线程或进程都持有一个锁资源并尝试锁定由链中下一个元素持有的资源时,就会发生死锁。例如,两个线程分别持有锁A和锁B,并且都试图获取另一个锁。


    我投票支持你的观点。你的回答更加简洁,因为它们指出死锁是由进程或线程引起的,这可能会让人感到困惑。有人说是进程,有人说是线程 :) - Hai Nguyen

    2

    死锁是指两个线程都获得了锁,这阻止了它们中的任何一个继续进行。避免死锁的最好方法是仔细开发。许多嵌入式系统通过使用看门狗定时器(一种定时器,每当系统挂起一段时间时就会重置系统)来防止死锁。


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