FSP语言学习(八):死锁

目录

1. 引言

2. Deadlock Analysis

3. Dining Philosophers Problem

3.1 Dining Philosophers Implementation

3.2 Deadlock-Free Philosophers


1. 引言

当一个系统的所有组成进程都被阻断时,就会出现死锁。另一种说法是,系统处于死锁状态,因为它没有可以执行的合格动作。我们已经在上一篇博客的嵌套监视器的例子中看到了死锁的例子。在那里,无论是生产者还是消费者进程都无法取得进一步的进展,因为消费者被封锁了,在等待生产者的字符,而生产者被封锁了,在等待消费者持有的监视器锁。换句话说,每个进程都在等待另一个进程持有的资源,有时被称为 "致命的拥抱"。Coffman、Elphick和Shoshani(1971)确定了发生死锁的四个必要和充分条件。

串行可重用的资源:所涉及的进程共享它们在相互排斥下使用的资源。例如,监视器封装了使用相互排斥访问的资源(即同步方法)。

递增式获取:进程在等待获取额外资源的同时坚持使用已经分配给它们的资源。这正是嵌套监控器死锁中的情况,消费者持有监控器锁,同时等待生产者将一个字符放入缓冲区。

没有抢占:一旦被进程获得,资源就不能被抢占(强行撤回),只能自愿释放。同样,这与嵌套的监控器死锁的关系很容易看出来,因为如果消费者线程可以被强迫释放监控器锁,那么生产者的执行就可以继续进行。

等待循环:存在一个循环的进程链(或循环),每个进程都持有一个资源,而循环中的后继者正在等待获得该资源。嵌套的监视器死锁是这种情况的一个具体例子,链的长度为2。消费者持有生产者正在等待的监控锁,生产者拥有消费者正在等待的字符。

处理死锁的策略包括确保这四个条件中的一个不成立。例如,考虑一个允许进程的死锁循环发生的系统,然后检测这些循环并中止死锁的进程,也许是通过操作者的行动。这样的系统否定了第三个条件(无抢占),因为被中止的进程被迫释放它们持有的资源。该系统实现了死锁检测和恢复。

在这里,我们关注的是另一种策略。我们的目的是设计程序,使死锁不能发生。我们描述了如何分析死锁的模型,以及如何证明一个模型是没有死锁的。最后,我们介绍了一个经典的并发编程问题的模型和实现,即Dining Philosophers。

2. Deadlock Analysis

在进程的有限状态模型中,僵局状态简单地说就是没有输出转换的状态。处于这种状态的进程不能再进行任何行动。在FSP中,这种死锁状态由本地进程STOP表示。下图描述了一个可能导致死锁状态的过程的例子。

上图的MOVE过程可以交替进行南、北行动。然而,一个北面跟着北面的动作序列会导致一个死锁状态,在这个状态下不可能有进一步的动作。这可以用Animator看到。我们可以要求LTSA分析工具找到死锁状态,并产生一个如何从起始状态达到这些状态的样本跟踪。通过对LTS图进行广度优先搜索,LTSA工具保证样本跟踪是通往死锁状态的最短跟踪。在上图的例子中,LTSA产生了以下输出。:

Trace to DEADLOCK:
     north
     north

一般来说,我们感兴趣的死锁不是那些像上面那样用STOP在原始进程中明确声明的死锁,而是那些由若干相互作用的原始进程的并行组合产生的死锁。当然,分析器对复合进程进行的死锁检查与对原始进程进行的检查是一样的,因为一个组合也是由LTS图描述的。这种检查仍然是对没有输出转换的状态的搜索。

下图的例子是一个系统,其中两个进程 P 和 Q 通过使用共享的打印机和共享的扫描仪执行相同的任务,即扫描文件并打印。每个进程都获得了打印机和扫描仪,执行扫描和打印,然后释放扫描仪和打印机资源。

下面两张图分别给出了进程P和共享打印机的LTS图。

进程P和Q之间的唯一区别是,P先获得打印机,Q先获得扫描仪。这个系统满足了死锁的四个必要和充分条件:扫描仪和打印机资源是连续重复使用的;每个进程在等待获得它所需要的第二个资源时,都坚持使用扫描仪或打印机;这些资源没有被抢占;从LTSA发现的以下死锁中可以看出等待周期。

Trace to DEADLOCK:
     p.printer.get
     q.scanner.get

在这种情况下,进程P拥有打印机并等待扫描仪,进程Q拥有扫描仪并等待打印机。在这个例子中,通过确保两个进程以相同的顺序请求打印机和扫描仪,可以很容易地解决这个死锁。(读者应该用LTSA来验证,如果图6.1的模型以这种方式被修改,死锁就不会再发生)。事实上,当进程共享不同类别的资源时,例如打印机和扫描仪,避免死锁的通用策略是对资源类别进行排序;也就是说,如果进程使用不同类别的资源,所有进程以相同的顺序获取这些资源。对于我们的例子,这可以通过,例如,总是在扫描器之前请求打印机来实现。

在这个例子中,避免死锁的另一个解决方案是为等待第二个资源设置一个超时。如果在超时时间内没有获得资源,那么第一个资源就会被释放,流程重新开始,如下图所示:

P          = (printer.get-> GETSCANNER),
GETSCANNER = (scanner.get->copy->printer.put
                         ->scanner.put->P
             |timeout -> printer.put->P
             ).
Q          = (scanner.get-> GETPRINTER),
GETPRINTER = (printer.get->copy->printer.put
                         ->scanner.put->Q
             |timeout -> scanner.put->Q
             ).

这就否定了增量采集的第二个死锁条件。这个解决方案可以用Java中的定时等待来实现。然而,这并不是一个好的解决方案,因为两个进程可以不断地获取第一个资源,超时,然后重复这个循环,在完成复制动作方面没有任何进展。LTSA检测到了这个进度问题,返回如下。

Progress violation for actions:
{p.scanner.get, p.copy, p.scanner.put,
q.printer.get, q.copy, q.printer.put}

我们在下面处理这类问题。

3. Dining Philosophers Problem

餐饮哲学家问题(Dijkstra, 1968a)是一个经典的并发编程问题,其中的死锁并不像前面的例子那么明显。我们同时开发了模型和Java实现。这个问题是这样表述的:五个哲学家共享一张圆形的桌子(如图所示),他们在这张桌子上有分配的座位。每个哲学家一生都在交替地思考和吃饭。桌子的中央是一大盘纠缠在一起的意大利面条。一个哲学家需要两把叉子才能吃下一份意大利面条。不幸的是,由于哲学不像计算机那样收入丰厚,哲学家们只能买得起五把叉子。每对哲学家之间各放一把叉子,他们同意每个人只使用他紧邻的左右两边的叉子。

这个系统中的资源是哲学家们之间共享的分叉。我们对分叉的建模方式与我们在上一节中对扫描仪和打印机资源的建模方式相同。

FORK = (get -> put -> FORK).

要使用一个叉子,哲学家必须首先拿起(得到)那个叉子,当用完叉子后,哲学家就把它放下(放)。每个哲学家都是以这个过程为模型的。

PHIL = (sitdown->right.get->left.get
          ->eat->left.put->right.put
          ->arise->PHIL).

换句话说,当一个哲学家饿了,他(或她)在桌前坐下,拿起他右边的叉子,如果它是空闲的,然后拿起他左边的叉子,如果它是空闲的。然后,哲学家就可以吃饭了。吃完后,哲学家释放叉子并离开桌子。如下图所示,现在可以用五个叉子过程和五个哲学家过程的组成来描述餐饮哲学家系统。

上图的图表可以简明地表达为复合过程。

||DINERS(N=5)=
   forall [i:0..N-1]
   (phil[i]:PHIL
   || {phil[i].left,phil[((i-1)+N)%N].right}::FORK).

表达式((i-1)+N)%N是减去N的模数,因此,比如说phil[0]和phil[4]之间共享一个分叉。对这个系统的分析显示了以下的死锁。

Trace to DEADLOCK:
     phil.0.sitdown
     phil.0.right.get
     phil.1.sitdown
     phil.1.right.get
     phil.2.sitdown
     phil.2.right.get
     phil.3.sitdown
     phil.3.right.get
     phil.4.sitdown
     phil.4.right.get

这就是所有的哲学家同时变得饥饿的情况,在餐桌前坐下,然后每个哲学家拿起他(或她)右边的叉子。由于每个哲学家都在等待他的邻居所持的叉子,所以系统无法取得进一步进展。换句话说,正如导言中所描述的,存在一个等待的循环。

3.1 Dining Philosophers Implementation

一般来说,实现一个错误的模型并不是一个好主意。然而,在这一节中,我们的目标是表明,虽然在模型中可以很容易地检测到死锁,但在对应于该模型的运行程序中却不是那么明显。在将Dining Philosophers模型转化为实现时,我们必须考虑模型中哪些进程将由被动对象(Monitor)表示,哪些由主动对象(threads)表示,叉子是这个系统中的被动实体,哲学家是主动实体。

下面的类图显示了参与 "用餐哲学家 "程序的各个类之间的关系。

该显示是由PhilCanvas类实现的。这个类所提供的接口在下面程序中给出。

class PhilCanvas extends Canvas {

  // set state of philosopher id to display state s,
  // method blocks calling thread if display is frozen
  synchronized void setPhil(int id,int s)
        throws java.lang.InterruptedException{}

  // "freeze" display
  synchronized void freeze(){}

  // "un-freeze" display
  synchronized void thaw() {}

  // set state of fork id to taken
  synchronized void setFork(int id, boolean taken){}
}

下个程序中列出了Fork监视器的Java实现。布尔变量,taken,编码叉子的状态。当叉子在桌子上时,taken为假。当叉子被哲学家拿起时,taken为真。

class Fork {
  private boolean taken=false;
  private PhilCanvas display;
  private int identity;

  Fork(PhilCanvas disp, int id)
    { display = disp; identity = id;}

  synchronized void put() {
    taken=false;
    display.setFork(identity,taken);
    notify();
  }

  synchronized void get()
     throws java.lang.InterruptedException {
    while (taken) wait();
    taken=true;
    display.setFork(identity,taken);
  }
}

哲学家线程的代码列在下个程序。它直接来自于模型。哲学家坐下和离开桌子的细节被省略了;哲学家是在坐在桌子上思考的。

class Philosopher extends Thread {
  private int identity;
  private PhilCanvas view;
  private Diners controller;
  private Fork left;
  private Fork right;

  Philosopher(Diners ctr,int id,Fork l,Fork r){
    controller = ctr; view = ctr.display;
    identity = id; left = l; right = r;
  }

  public void run() {
    try {
      while (true) {
        // thinking
        view.setPhil(identity,view.THINKING);
        sleep(controller.sleepTime());
        // hungry
        view.setPhil(identity,view.HUNGRY);
        right.get();
        // gotright fork
        view.setPhil(identity,view.GOTRIGHT);
        sleep(500);
        left.get();
        // eating
        view.setPhil(identity,view.EATING);
        sleep(controller.eatTime());
        right.put();
        left.put();
      }
    } catch (java.lang.InterruptedException e){}
  }
}

哲学家花在思考和吃饭上的时间由小程序显示中的滑块控制:

创建Philosopher线程和Fork监视器的代码是:

for (int i =0; i<N; ++i)
  fork[i] = new Fork(display,i);
for (int i =0; i<N; ++i){
  phil[i] =
    new Philosopher(this,i,fork[(i-1+N)%N],fork[i]);
  phil[i].start();
}

下图描述了Dining Philosophers小程序的运行。在死锁发生之前,小程序可能会运行很长时间。为了确保死锁最终发生,可以将滑块控制移到左边。这减少了哲学家们思考和吃饭的时间,这种 "加速 "增加了死锁发生的概率。下图描述了死锁发生时的小程序。很明显,每个哲学家都在抓着他右边的叉子。

3.2 Deadlock-Free Philosophers

对于 "吃饭的哲学家 "问题,有许多不同的解决方案。所有的解决方案都涉及到否认本篇开始时确定的四个僵局的必要和充分条件之一。我们在这里概述的解决方案取决于确保等待周期不存在。为了做到这一点,我们在哲学家的定义中引入了一些不对称性。到现在为止,每个哲学家都有一个相同的定义。我们让奇数的哲学家先得到右边的叉子,偶数的哲学家先得到左边的叉子。修订后的模型列在下面。

FORK = (get -> put -> FORK).

PHIL(I=0)
    = (when (I%2==0) sitdown
         ->left.get->right.get
         ->eat->left.put->right.put->arise->PHIL
      |when (I%2==1) sitdown
         ->right.get->left.get
         ->eat->left.put->right.put->arise->PHIL
      ).

||DINERS(N=5)=
   forall [i:0..N-1]
   (phil[i]:PHIL(i)
   || {phil[i].left,phil[((i-1)+N)%N].right}::FORK).

这个关于Dining Philosophers的规范是无死锁的,因为不再可能存在等待循环,在这个循环中,每个哲学家都持有右边的叉子。可以用LTSA验证上述模型实际上是无死锁的。当然也可以对Java实现做同样的改变,结果是一个无死锁的程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值