ChatGPT解决这个技术问题 Extra ChatGPT

死锁和活锁有什么区别?

有人可以用示例(代码)解释死锁和活锁有什么区别吗?


C
Community

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

在并发计算中,死锁是一组动作的每个成员都在等待其他成员释放锁的状态。活锁类似于死锁,只是活锁所涉及的进程的状态不断变化关于彼此,没有进展。活锁是资源匮乏的一种特殊情况;一般定义仅说明特定进程没有进展。一个真实的活锁示例发生在两个人在狭窄的走廊里相遇,每个人都试图通过移动让对方通过,但他们最终左右摇摆而没有任何进展,因为他们都反复移动在同一时间以同样的方式。对于一些检测死锁并从死锁中恢复的算法来说,活锁是一种风险。如果有多个进程采取行动,死锁检测算法会被反复触发。这可以通过确保只有一个进程(随机或按优先级选择)采取行动来避免。


我已经找到了,但他们没有你可以看到的例子,无论如何谢谢
我不会提供代码示例,但考虑两个进程,每个进程都在等待另一个资源,但以非阻塞方式等待。当每个人都知道他们无法继续时,他们释放他们持有的资源并休眠 30 秒,然后他们检索他们的原始资源,然后尝试其他进程持有的资源,然后离开,然后重新获取。由于这两个进程都试图应对(只是很糟糕),这是一个活锁。
你能给我同样的例子,但有死锁,提前谢谢
死锁示例要简单得多……假设两个进程 A 和 B,每个进程都需要资源 r1 和资源 r2。假设 A 收到(或已经拥有)r1,而 B 收到(或已经拥有)r2。现在每个人都尝试获取另一个人拥有的资源,没有任何超时。 A 被阻塞是因为 B 持有 r2,B 被阻塞是因为 A 持有 r1。每个进程都被阻塞,因此无法释放其他进程想要的资源,从而导致死锁。
在事务性内存的上下文中,有一个很棒的视频演示了死锁和活锁:youtube.com/watch?v=_IxsOEEzf-c
C
Community

Livelock

一个线程经常响应另一个线程的动作。如果另一个线程的动作也是对另一个线程动作的响应,那么可能会导致活锁。与死锁一样,活锁线程无法取得进一步进展。但是,线程并没有被阻塞——它们只是忙于相互响应而无法恢复工作。这类似于两个人试图在走廊里互相超越:Alphonse 向左移动让 Gaston 通过,而 Gaston 向右移动让 Alphonse 通过。阿尔方斯看到他们仍然互相阻挡,他向右移动,而加斯顿则向左移动。他们仍然互相阻挡,等等......

活锁和死锁的主要区别在于线程不会被阻塞,而是会尝试不断地相互响应。

在此图像中,两个圆圈(线程或进程)都将尝试通过左右移动来给对方留出空间。但他们不能再进一步了。

https://i.stack.imgur.com/OAda8.jpg


这东西有个名字。也许是一个俚语,但仍然是:schlumperdink:P
D
Daniel Frederico Lins Leite

这里所有的内容和例子都来自

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

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

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

活锁:两个或多个进程不断改变其状态以响应其他进程的变化而不做任何有用工作的情况:

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

假设三个进程(P1、P2、P3)每个都需要定期访问资源 R。考虑 P1 拥有资源的情况,而 P2 和 P3 都被延迟,等待该资源。当 P1 退出其临界区时,应允许 P2 或 P3 访问 R。假设操作系统授予对 P3 的访问权限,并且 P1 在 P3 完成其临界区之前再次需要访问。如果操作系统在 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 将 flag[0] 设置为真。

P1 将 flag[1] 设置为真。

P0 检查标志[1]。

P1 检查标志[0]。

P0 将 flag[0] 设置为 false。

P1 将 flag[1] 设置为 false。

P0 将 flag[0] 设置为真。

P1 将 flag[1] 设置为真。

这个序列可以无限延长,而且任何一个进程都不能进入它的临界区。严格来说,这不是死锁,因为两个进程相对速度的任何改变都会打破这个循环,让一个进程进入临界区。这种情况称为活锁。回想一下,当一组进程希望进入其临界区但没有进程成功时,就会发生死锁。使用活锁,有可能成功的执行序列,但也可以描述一个或多个执行序列,其中没有进程进入其临界区。

不再是书中的内容。

那么自旋锁呢?

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

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/
Is my spin lock implementation correct and optimal?

概括:

死锁:没有人进步,什么都不做(睡觉,等待等)的情况。 CPU使用率会很低;

Livelock:没有人进展,但 CPU 用于锁定机制而不是您的计算的情况;

饥饿:一个进程永远没有机会运行的情况;纯粹是运气不好或它的某些属性(例如,低优先级);

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


先生,您为死锁提供的示例实际上是自旋锁的示例。当一组未处于就绪或运行状态并等待某些资源的进程被阻塞时,就会发生死锁。但是在我们的示例中,每个人都在执行一些任务,即一次又一次地检查条件。如果我错了,请纠正我。
该示例是如此之少,以至于为这种解释提供了机会,因此我对其进行了改进,以更加明确地说明它们的区别。希望有帮助。
感谢您添加有关自旋锁的信息,根据您的说法,自旋锁是一种技术,您也对其进行了证明,我理解。但是,当一个进程 P1 处于临界区而其他高优先级进程 P2 被调度抢占 P1 时,优先级反转问题又如何呢?在这种情况下,CPU 与 P2 在一起,而我们的同步机制与 P1 在一起。这称为自旋锁,因为 P1 处于就绪状态而 P2 处于运行状态。这里自旋锁是一个问题。我做对了吗?我无法正确处理错综复杂的事情。请帮忙
我对您的建议是创建另一个问题,更清楚地说明您的问题。现在,如果您在“用户空间”中,并且 P1 位于一个关键会话中,该会话受一个使用无限循环实现的 SpinLock 保护并且它被抢占;然后 P2 会尝试进入它,但会失败并会烧掉它的所有时间片。您创建了一个活锁(一个 CPU 将处于 100%)。 (一个不好的用途是使用自旋锁保护同步 IO。您可以轻松尝试此示例)在“内核空间”上,也许此注释可以帮助您:lxr.linux.no/linux+v3.6.6/Documentation/…
非常感谢您的澄清。无论如何,与其他人不同,您的回答非常具有描述性和帮助性
D
Deepak Lamichhane

DEADLOCK 死锁是一个任务无限期地等待永远无法满足的条件 - 任务声称对共享资源的独占控制 - 任务在等待其他资源被释放的同时持有资源 - 不能强制任务释放资源 - 循环等待条件存在

LIVELOCK 当两个或多个任务依赖并使用某个资源时,可能会出现活锁条件,从而导致循环依赖条件,这些任务将永远继续运行,从而阻止所有较低优先级的任务运行(这些较低优先级的任务会遇到一种称为饥饿的情况)


如果“活锁”任务遵循包括“退避”延迟的资源仲裁协议,结果大部分时间都处于睡眠状态,那么其他任务将不会被饿死。
m
mmirwaldt

也许这两个例子说明了死锁和活锁之间的区别:

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)重复。
我怀疑您认为的活锁是否真的存在足够长的时间以至于它们会引起麻烦。当你总是放弃你持有的所有锁而你不能分配下一个锁时,死锁(和活锁)条件“持有并等待”就消失了,因为实际上不再有等待了。 (en.wikipedia.org/wiki/Deadlock)
确实缺少死锁条件,因为这些是我们正在讨论的活锁。我给出的示例类似于给出的标准走廊示例:geeksforgeeks.org/deadlock-starvation-and-livelocken.wikibooks.org/wiki/Operating_System_Design/Concurrency/…docs.oracle.com/javase/tutorial/essential/concurrency/…
第一个链接中的代码示例错过了解锁语句,这让我不知何故感到非常困惑。目前尚不清楚关键部分从哪里开始以及在哪里结束。我的观点是每次尝试时语句的执行顺序都会发生变化,实际上在下一个回合它永远不会相同。并不是每个执行命令最后都会导致活锁。他们中的大多数甚至没有!所以你不会观察到这些活锁,因为它们很快就会随着下一个无害的执行顺序消失得很快,这很可能。没有完美的活锁示例可以观察到 ;-)
这取决于动作的触发器以及它们需要多长时间。它绝对可以是一个有效的锁。如果需要 10 秒的计算来跳转到一个状态或返回,并且两个线程以 5 秒的相位差相互反应,那么计算速度在同一进程中的两个线程之间变化的机会足以将其推开5秒太少了。自己试试吧。创建两个运行 10 秒的程序,并以 5 秒的间隔启动它们,看看它们需要多长时间才能在一定的余量(比如 1 秒)内进入相位。
s
stdout

想象一下,你有线程 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 循环并持有锁时,它会执行它必须做的事情并将 commonVar 设置为 true。然后线程 B 进来,进入 while 循环,由于 commonVar 现在是 true,它能够持有锁。它这样做,执行 synchronised 块,并将 commonVar 设置回 false。现在,线程 A 再次获得它的新 CPU 窗口,它要退出 while 循环,但线程 B 刚刚将其设置回 false,因此循环再次重复。线程做一些事情(所以它们不会在传统意义上被阻塞)但几乎没有。

值得一提的是,活锁不一定要出现在这里。我假设一旦 synchronised 块完成执行,调度程序就会支持另一个线程。大多数时候,我认为这是一个难以实现的期望,并且取决于幕后发生的许多事情。


很好的例子。它还说明了为什么您应该始终在并发上下文中以原子方式读写。如果 while 循环位于同步块内,则上述内容不会成为问题。
R
RCvaram

我只是打算分享一些知识。

死锁 如果集合中的每个线程/进程都在等待只有该集合中的另一个进程可以引发的事件,那么一组线程/进程就会死锁。

这里重要的是另一个进程也在同一个集合中。这意味着另一个进程也被阻止,没有人可以继续。

当进程被授予对资源的独占访问权限时,就会发生死锁。

必须满足这四个条件才有死锁。

互斥条件(每个资源分配给1个进程) 保持等待条件(进程持有资源,同时可以请求其他资源)。无抢占条件(不能强行夺走之前授予的资源) # 它会动态发生

如果我们找到了这些条件,那么我们可以说可能会发生像死锁这样的情况。

活锁

每个线程/进程一次又一次地重复相同的状态,但没有进一步进展。由于进程无法进入临界区,因此类似于死锁。然而,在死锁中,进程处于等待状态,除了在活锁中之外什么都不做,进程试图继续,但进程一次又一次地重复到相同的状态。

(在死锁计算中,没有可能成功的执行序列。但在活锁计算中,有成功的计算,但是有一个或多个执行序列,其中没有进程进入其临界区。)

死锁和活锁的区别

当死锁发生时,不会发生执行。但是在活锁中,会发生一些执行,但这些执行不足以进入临界区。