死锁和活锁有什么区别?


Answers:


398

取自http://en.wikipedia.org/wiki/Deadlock

在并发计算中,死锁是一种状态,其中一组动作的每个成员都在等待某个其他成员释放锁

活锁类似于死锁,不同之处在于所涉及的活锁的过程的状态不断地关于彼此,无进展而改变。Livelock是资源匮乏的特例;一般定义仅指出特定过程没有进展。

当两个人在狭窄的走廊里相遇时,发生了现实生活中的活锁例子,每个人都试图通过移动到一边让对方经过而礼貌,但是最终却没有任何进展就左右摇摆,因为他们都反复移动在同一时间相同的方式。

对于某些检测死锁并从死锁中恢复的算法,活锁是一种风险。如果有多个进程采取措施,则死锁检测算法可以重复触发。通过确保只有一个过程(随机选择或优先选择)可以采取措施,可以避免这种情况。


8
我已经找到了,但是他们那里没有示例,反正您还是可以看到
macindows 2011年

61
我将不提供代码示例,而是考虑两个进程,每个进程在等待另一个资源,但是以非阻塞方式等待。当每个人都知道自己无法继续时,他们释放其持有的资源并睡眠30秒钟,然后他们检索其原始资源,然后尝试使用另一个进程持有的资源,然后离开,然后重新获得。由于这两个过程都在试图应对(严重),因此这是一个活锁。
麻将

4
您能给我同样的例子,但是有僵局,在此先感谢
macindows 2011年

32
死锁示例容易得多...假设有两个进程A和B,并且每个进程都需要资源r1和资源r2。假设A接收(或已经拥有)r1,而B接收(或已经拥有)r2。现在,每个尝试获取彼此拥有的资源,而没有任何超时。A因B持有r2而被阻塞,而B因A持有r1而被阻塞。每个进程都被阻止,因此无法释放其他进程想要的资源,从而导致死锁。
麻将

2
在事务性内存的上下文中,有一段精彩的视频演示了死锁和活锁:youtube.com/watch?
v=_IxsOEEzf

78

活锁

一个线程通常会响应另一个线程的操作而行动。如果另一个线程的动作也是对另一个线程的动作的响应,则可能导致活动锁。

与死锁一样,活锁的线程无法取得进一步的进展。但是,线程并未被阻塞 -它们只是太忙于彼此响应而无法恢复工作。这相当于两个人试图在走廊中互相经过:阿方斯(Alphonse)向左移动以让加斯顿(Gaston)通过,而格斯顿(Gaston)向右移动以让Alphonse通过。看到他们仍然互相阻挡,阿方斯(Alphonse)向右移动,而加斯顿(Gaston)向左移动。他们仍然互相阻碍,依此类推...

活锁死锁之间的主要区别在于,线程不会被阻塞,而是将尝试不断进行响应。

在此图中,两个圆圈(线程或进程)将尝试通过左右移动来给另一个空间。但是他们不能再前进了。

在此处输入图片说明



1
这个东西有个名字。也许但还是个语:schlumperdink:P
John Red

64

这里的所有内容和示例均来自

操作系统:内部结构和设计原则
William Stallings8º

死锁:一种情况,其中两个或多个进程无法进行,因为每个进程都在等待另一个进程在做某事。

例如,考虑两个进程P1和P2,以及两个资源R1和R2。假设每个进程都需要访问两个资源才能执行其部分功能。然后可能出现以下情况:OS将R1分配给P2,将R2分配给P1。每个进程都在等待两种资源之一。在获得另一资源并执行需要这两种资源的功能之前,它们都不会释放它已经拥有的资源。这两个过程陷入僵局

活锁(Livelock):两个或多个进程在不做任何有用工作的情况下,不断响应其他进程的更改而更改其状态的情况:

饥饿:调度程序无限期地忽略可运行进程的情况;尽管它可以继续进行,但从未选择。

假设三个进程(P1,P2,P3)每个都需要定期访问资源R。请考虑以下情况:P1拥有该资源,并且P2和P3都被延迟,等待该资源。当P1退出其关键部分时,应允许P2或P3访问R。假定OS授予对P3的访问权限,并且P1在P3完成其关键部分之前再次需要访问。如果OS在P3完成之后授予对P1的访问权限,然后又交替授予对P1和P3的访问权限,那么即使没有死锁情况,P2也可能无限期地被拒绝访问资源。

附录A-同步主题

死锁示例

如果两个进程都在执行while语句之前将其标志设置为true,则每个进程都将认为另一个进程已进入其临界区,从而导致死锁。

/* PROCESS 0 */
flag[0] = true;            // <- get lock 0
while (flag[1])            // <- is lock 1 free?
    /* do nothing */;      // <- no? so I wait 1 second, for example
                           // and test again.
                           // on more sophisticated setups we can ask
                           // to be woken when lock 1 is freed
/* critical section*/;     // <- do what we need (this will never happen)
flag[0] = false;           // <- releasing our lock

 /* PROCESS 1 */
flag[1] = true;
while (flag[0])
    /* do nothing */;
/* critical section*/;
flag[1] = false;

活锁示例

/* PROCESS 0 */
flag[0] = true;          // <- get lock 0
while (flag[1]){         
    flag[0] = false;     // <- instead of sleeping, we do useless work
                         //    needed by the lock mechanism
    /*delay */;          // <- wait for a second
    flag[0] = true;      // <- and restart useless work again.
}
/*critical section*/;    // <- do what we need (this will never happen)
flag[0] = false; 

/* PROCESS 1 */
flag[1] = true;
while (flag[0]) {
    flag[1] = false;
    /*delay */;
    flag[1] = true;
}
/* critical section*/;
flag[1] = false;

[...]考虑以下事件顺序:

  • P0将标志[0]设置为true。
  • P1将flag [1]设置为true。
  • P0检查标志[1]。
  • P1检查标志[0]。
  • P0将标志[0]设置为false。
  • P1将flag [1]设置为false。
  • P0将标志[0]设置为true。
  • P1将flag [1]设置为true。

此序列可以无限期地扩展,任何过程都不能进入其关键部分。严格来说,这不是死锁,因为这两个过程的相对速度的任何改变都将打破此循环并允许一个进入关键部分。这种情况称为“ 活锁”。回想一下,当一组进程希望进入其关键部分但没有成功的进程时,就会发生死锁。使用livelock时,可能会有成功的执行序列,但是也可能描述一个或多个执行序列,其中没有进程进入其关键部分。

不再满足于此书。

那自旋锁呢?

自旋锁是一种避免操作系统锁定机制成本的技术。通常,您会这样做:

try
{
   lock = beginLock();
   doSomething();
}
finally
{
   endLock();
}

beginLock()成本远高于成本时,问题开始出现doSomething()。用非常夸张的术语,想象一下当beginLock花费1秒但doSomething仅花费1毫秒时会发生什么。

在这种情况下,如果您等待1毫秒,则可以避免受到1秒钟的阻碍。

为什么beginLock要花这么多钱?如果没有锁是免费的(请参阅https://stackoverflow.com/a/49712993/5397116),但是如果没有锁,则操作系统将“冻结”您的线程,请设置一种机制来唤醒您释放锁后,以后再唤醒您。

所有这些都比某些检查锁的循环要昂贵得多。这就是为什么有时最好做一个“自旋锁”的原因。

例如:

void beginSpinLock(lock)
{
   if(lock) loopFor(1 milliseconds);
   else 
   {
     lock = true;
     return;
   }

   if(lock) loopFor(2 milliseconds);
   else 
   {
     lock = true;
     return;
   }

   // important is that the part above never 
   // cause the thread to sleep.
   // It is "burning" the time slice of this thread.
   // Hopefully for good.

   // some implementations fallback to OS lock mechanism
   // after a few tries
   if(lock) return beginLock(lock);
   else 
   {
     lock = true;
     return;
   }
}

如果您的实现不仔细,您可能会陷入动态锁定,将所有CPU都花在锁定机制上。

另请参阅:

https://preshing.com/20120226/roll-your-own-lightweight-mutex/
我的自旋锁实现正确且最佳吗?

总结

死锁:无人进步,无所事事(睡觉,等待等)的情况。CPU使用率会很低;

Livelock:这种情况下,没有任何进展,但是CPU花费在锁定机制上而不是计算上;

饥饿:一个女主角永远没有机会奔跑的情况;纯粹由于运气不好或由于其某些性质(例如,低优先级);

自旋锁(Spinlock):避免等待释放锁的成本的技术。


先生,您为死锁提供的示例实际上是Spinlock的示例。当一组未处于就绪或运行状态并正在等待某些资源的进程被阻塞时,就会发生死锁。但是在我们的示例中,每个人都在执行某些任务,即一次又一次地检查条件。如果我错了,请纠正我。
Vinay Yadav

这个例子很小,为解释提供了机会,因此我改进了它,使它们的区别更加明确。希望能有所帮助。
Daniel Frederico Lins Leite

感谢您添加有关自旋锁的信息,根据您的说法,自旋锁是一种技术,您也对此进行了论证,我了解。但是,当一个进程P1处于关键部分,而另一个高优先级进程P2被调度抢占P1时,该优先级反转问题又如何呢?在这种情况下,CPU使用P2,而我们的同步机制使用P1。由于P1处于就绪状态而P2处于运行状态,因此称为自旋锁。在这里自旋锁是个问题。我做对了吗?我无法弄清错综复杂的内容。请帮助
Vinay Yadav

我对您的建议是创建另一个问题,以更清楚地说明您的问题。现在,如果您位于“用户空间”中,并且P1处于关键会话中,该会话由使用无限循环实现且已被抢占的SpinLock保护;然后P2将尝试输入它,否则将失败,并将消耗其所有时间片。您创建了一个活锁(一个CPU将处于100%状态)。(一个不好的用法是使用spinlock保护同步IO。您可以轻松尝试以下示例)在“内核空间”上,此注释可能会对
Daniel Frederico Lins Leite

非常感谢您的澄清。无论如何,您的回答与其他人不同,是非常有描述性的和有用的
Vinay Yadav

13

DEADLOCK 死锁是一种情况,其中任务无限期地等待着永远无法满足的条件-任务要求对共享资源的独占控制权-任务在等待其他资源释放时保留资源-不能强迫任务重新分配资源-循环等待条件存在

LIVELOCK 当两个或两个以上任务依赖并使用某些资源并导致循环依赖条件时,这些任务将永远运行,从而导致所有优先级较低的任务无法运行(这些优先级较低的任务遇到一种称为饥饿的条件),则可能会出现Livelock条件


如果“活动锁定”任务遵循资源仲裁协议(其中包括“退避”延迟),并且大部分时间都处于睡眠状态,那么其他任务将不会饿死。
greggo,2015年

8

也许这两个示例向您说明了死锁和活动锁之间的区别:


Java-死锁示例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class DeadlockSample {

    private static final Lock lock1 = new ReentrantLock(true);
    private static final Lock lock2 = new ReentrantLock(true);

    public static void main(String[] args) {
        Thread threadA = new Thread(DeadlockSample::doA,"Thread A");
        Thread threadB = new Thread(DeadlockSample::doB,"Thread B");
        threadA.start();
        threadB.start();
    }

    public static void doA() {
        System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
        lock1.lock();
        System.out.println(Thread.currentThread().getName() + " : holds lock 1");

        try {
            System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
            lock2.lock();
            System.out.println(Thread.currentThread().getName() + " : holds lock 2");

            try {
                System.out.println(Thread.currentThread().getName() + " : critical section of doA()");
            } finally {
                lock2.unlock();
                System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
            }
        } finally {
            lock1.unlock();
            System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
        }
    }

    public static void doB() {
        System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
        lock2.lock();
        System.out.println(Thread.currentThread().getName() + " : holds lock 2");

        try {
            System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
            lock1.lock();
            System.out.println(Thread.currentThread().getName() + " : holds lock 1");

            try {
                System.out.println(Thread.currentThread().getName() + " : critical section of doB()");
            } finally {
                lock1.unlock();
                System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
            }
        } finally {
            lock2.unlock();
            System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
        }
    }
}

样本输出:

Thread A : waits for lock 1
Thread B : waits for lock 2
Thread A : holds lock 1
Thread B : holds lock 2
Thread B : waits for lock 1
Thread A : waits for lock 2

活锁的Java示例:


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LivelockSample {

    private static final Lock lock1 = new ReentrantLock(true);
    private static final Lock lock2 = new ReentrantLock(true);

    public static void main(String[] args) {
        Thread threadA = new Thread(LivelockSample::doA, "Thread A");
        Thread threadB = new Thread(LivelockSample::doB, "Thread B");
        threadA.start();
        threadB.start();
    }

    public static void doA() {
        try {
            while (!lock1.tryLock()) {
                System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
                Thread.sleep(100);
            }
            System.out.println(Thread.currentThread().getName() + " : holds lock 1");

            try {
                while (!lock2.tryLock()) {
                    System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
                    Thread.sleep(100);
                }
                System.out.println(Thread.currentThread().getName() + " : holds lock 2");

                try {
                    System.out.println(Thread.currentThread().getName() + " : critical section of doA()");
                } finally {
                    lock2.unlock();
                    System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
                }
            } finally {
                lock1.unlock();
                System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
            }
        } catch (InterruptedException e) {
            // can be ignored here for this sample
        }
    }

    public static void doB() {
        try {
            while (!lock2.tryLock()) {
                System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
                Thread.sleep(100);
            }
            System.out.println(Thread.currentThread().getName() + " : holds lock 2");

            try {
                while (!lock1.tryLock()) {
                    System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
                    Thread.sleep(100);
                }
                System.out.println(Thread.currentThread().getName() + " : holds lock 1");

                try {
                    System.out.println(Thread.currentThread().getName() + " : critical section of doB()");
                } finally {
                    lock1.unlock();
                    System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
                }
            } finally {
                lock2.unlock();
                System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
            }
        } catch (InterruptedException e) {
            // can be ignored here for this sample
        }
    }
}

样本输出:

Thread B : holds lock 2
Thread A : holds lock 1
Thread A : waits for lock 2
Thread B : waits for lock 1
Thread B : waits for lock 1
Thread A : waits for lock 2
Thread A : waits for lock 2
Thread B : waits for lock 1
Thread B : waits for lock 1
Thread A : waits for lock 2
Thread A : waits for lock 2
Thread B : waits for lock 1
...

这两个示例都强制线程以不同的顺序获取锁。当死锁等待另一个锁时,活动锁实际上并没有真正等待-它拼命尝试获取该锁而没有机会获得它。每次尝试都会消耗CPU周期。


代码很好。但是活锁的例子并不好。线程是在值上被阻塞还是正在轮询值的更改在概念上没有区别。为了更好地说明活动锁,一个简单的更改是让线程A和B在意识到无法获得所需的第二个锁时释放它们所拥有的锁。然后他们每个睡眠一秒钟,重新获得他们原来拥有的锁,然后再睡眠一秒钟,然后尝试再次获取另一个锁。因此,每个周期将是:1)获得矿山,2)睡眠,3)尝试获得其他并失败,4)释放矿山,5)睡眠,6)重复。
CognizantApe

1
我怀疑您想到的活锁是否真的存在足够长的时间,以至于会引起麻烦。当您在无法分配下一个锁时总是放弃所有持有的锁时,死锁(和活动锁)条件“保持并等待”将丢失,因为实际上不再需要等待。(en.wikipedia.org/wiki/Deadlock
mmirwaldt

确实缺少了死锁条件,因为这些是我们正在讨论的活锁。我给的例子类似于给出的标准走廊例如:geeksforgeeks.org/deadlock-starvation-and-livelocken.wikibooks.org/wiki/Operating_System_Design/Concurrency/...docs.oracle.com/javase/tutorial/essential / concurrency /…
CognizantApe

0

假设您有线程A和线程B。它们都synchronised在同一个对象上,并且在此块中有一个全局变量,它们都在更新;

static boolean commonVar = false;
Object lock = new Object;

...

void threadAMethod(){
    ...
    while(commonVar == false){
         synchornized(lock){
              ...
              commonVar = true
         }
    }
}

void threadBMethod(){
    ...
    while(commonVar == true){
         synchornized(lock){
              ...
              commonVar = false
         }
    }
}

因此,当线程A进入while循环并持有锁时,它将执行它必须做的并将其设置commonVartrue。然后线程B进来,在进入while循环,因为commonVartrue现在,它是能够持有锁。这样做,执行该synchronised块,并将其设置commonVarfalse。现在,线程A再次得到它的新的CPU窗口,它要退出while循环,但线程B刚刚设置回false,如此循环重复了一遍。线程可以执行某些操作(因此在传统意义上不会被阻塞),但几乎没有任何作用。

可能还需要提及的是,活锁不一定必须出现在此处。我假设一旦synchronised块完成执行,调度程序就会偏向另一个线程。在大多数情况下,我认为这是一个很难达到的期望,它取决于引擎盖下发生的许多事情。


很好的例子。它还说明了为什么您应该始终在并发上下文中自动进行读写。如果while循环位于syncize块内,那么上述内容就不会成为问题。
CognizantApe
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.