死锁、饥饿和活锁
先决条件 - 死锁和饥饿
当两个或多个进程不断重复相同的交互以响应其他进程的变化而不做任何有用的工作时,就会发生活锁。这些进程不处于等待状态,它们是并发运行的。这与死锁不同,因为在死锁中所有进程都处于等待状态。
例子:
想象一下使用两种资源的一对进程,如图所示:
void process_A(void)
{
enter_reg(& resource_1);
enter_reg(& resource_2);
use_both_resources();
leave_reg(& resource_2);
leave_reg(& resource_1);
}
void process_B(void)
{
enter_reg(& resource_1);
enter_reg(& resource_2);
use_both_resources();
leave_reg(& resource_2);
leave_reg(& resource_1);
}
这两个进程中的每一个都需要这两种资源,并且它们使用轮询原语 enter_reg 来尝试获取它们所需的锁。如果尝试失败,该过程将再次尝试。
如果进程A先运行并获取资源1,然后进程B运行并获取资源2,那么无论接下来运行哪一个,都不会继续前进,但两个进程都不会阻塞。实际发生的情况是它一次又一次地用完它的 CPU 量,没有任何进展,也没有任何阻塞。因此,这种情况不是死锁(因为没有进程被阻塞),但我们有一些功能上等同于死锁的东西:LIVELOCK。
什么导致活锁?
活锁的发生可能以最令人惊讶的方式发生。某些系统中允许的进程总数由进程表中的条目数决定。因此进程表槽可以称为有限资源。如果由于表已满而导致分叉失败,则等待随机时间并重试对于执行分叉的程序来说是一种合理的方法。
考虑一个有 100 个进程槽的 UNIX 系统。十个程序正在运行,每个程序都必须创建 12 个(子)进程。每个进程创建了9个进程后,原来的10个进程和新的90个进程已经用完了表。 10 个原始进程中的每一个现在都处于无限循环中,分叉和失败——这正是死锁的情况。这种情况发生的可能性很小,但有可能发生。
死锁、饥饿和活锁的区别:
活锁类似于死锁,不同之处在于活锁中涉及的进程的状态不断变化,没有进展。 Livelock 是一种资源匮乏的特例;一般定义仅说明特定过程没有进展。
活锁:
var l1 = .... // lock object like semaphore or mutex etc
var l2 = .... // lock object like semaphore or mutex etc
// Thread1
Thread.Start( ()=> {
while (true) {
if (!l1.Lock(1000)) {
continue;
}
if (!l2.Lock(1000)) {
continue;
}
/// do some work
});
// Thread2
Thread.Start( ()=> {
while (true) {
if (!l2.Lock(1000)) {
continue;
}
if (!l1.Lock(1000)) {
continue;
}
// do some work
});
僵局:
var p = new object();
lock(p)
{
lock(p)
{
// deadlock. Since p is previously locked
// we will never reach here...
}
死锁是一组动作的每个成员都在等待其他成员释放锁的状态。另一方面,活锁几乎类似于死锁,除了活锁中涉及的进程的状态不断地相互改变,没有任何进展。因此 Livelock 是资源匮乏的一种特殊情况,正如一般定义所述,该过程没有进展。
饥饿:
饥饿是一个与活锁和死锁密切相关的问题。在动态系统中,对资源的请求不断发生。因此,需要一些策略来决定谁在何时获得资源。这个过程是合理的,可能会导致一些进程永远不会得到服务,即使它们没有死锁。
Queue q = .....
while (q.Count & gt; 0)
{
var c = q.Dequeue();
.........
// Some method in different thread accidentally
// puts c back in queue twice within same time frame
q.Enqueue(c);
q.Enqueue(c);
// leading to growth of queue twice then it
// can consume, thus starving of computing
}
当“贪婪”线程使共享资源长时间不可用时,就会发生饥饿。例如,假设一个对象提供了一个通常需要很长时间才能返回的同步方法。如果一个线程频繁调用这个方法,其他同样需要频繁同步访问同一对象的线程也会经常被阻塞。