哲学家就餐问题

1.进程互斥与同步,死锁基本知识

在多道程序环境下,进程有异步和同步两种并发执行方式。异步执行是指运行中的各进程在操作系统的调度下以不可预知的速度向前推进。异步执行的进程大多没有时序要求,不存在“执行结果与语句的特定执行顺序有关”的条件竞争。然而存在一类协作进程,“保证数据的一致性” 的前提要求它们必须按某种特定顺序执行,并且遵守如下两种限制。 
(1)R1(顺序化执行):进程A 的eventA事件必须发生在进程B的eventB事件之前; 
(2)R2(互斥执行):进程 A的eventA事件与进程B的eventB事件不能同时发生。把上述限制下多进程的运行状态叫作进程的同步执行。进程同步执行时因存在着明显的执行上的时序要求而相互等待。如果说进程异步是进程并发执行的自然结果,那么进程同步则需要程序员通过准确嵌入一些诸如加解锁来确保实现。 
信号量无疑是一个较为理想的同步工具。它最早由荷兰科学家EdsgerDijkstra于1965年提出,该工具具有如下三个优点:(1)仅需要两个基本操作即可完成进程的同步和互斥,而且两个原子操作代码简洁高效, 易于扩充;(2) 精心设计的信号量对象类似一条条“触发器”规则,加上信号量机制的强制作用可以帮助程序员少犯错误;(3)信号量已在很多系统中实现,解决方案中有意识地选用信号量无疑将使进程更“瘦身”,运行更高效。信号量技术的引入是对早期忙等型(busywaiting)进程控制变量是个巨大的提升,但在使用过程中仍然存在不少缺点:一是不能随时读取信号量的值, 必要时须重复定义一个跟踪信号量值的普通变量,二是程序员对信号量的PV操作的正确使用与否没有任何控制和保证(后来引入管程和条件变量,PV操作完全由编译器而非 程序员安排),不合理地使用将导致进程饥饿甚至死锁。死锁应尽可能阻止,系统死锁导致诸进程将进入无法向前推进的僵持状态, 除非借助于外力。死锁的原因除了系统资源偏少之外,更多的是进程推进速度不当, 或者说进程申请和释放信号量的顺序不合理所致,毕竟系统提供的资源是有限的。以哲学家就餐问题为例,若派发给每位哲学家一双筷子(更准确地说,6支就足够), 则一定不会死锁。事实上,若信号量的PV操作顺序处置得当,5支筷子同样也可以保证不会发生死锁。 
死锁是 《操作系统原理》课程中的1个很重要的概念, 它描述的是多个进程因竞争资源而造成的1种僵局 ,若无外力作用 ,这些进程将永远不能再向前推进。产生死锁的原因主要有2点: 1是竞争资源 ; 2是进程推进顺序不当。

2.哲学家就餐问题

哲学家就餐问题是在计算机科学中的一个经典问题,用来演示在并行计算中多线程同步(Synchronization)时产生的问题。在1971年,著名的计算机科学家艾兹格•迪科斯彻提出了一个同步问题,即假设有五台计算机都试图访问五份共享的磁带驱动器。稍后,这个问题被托尼•霍尔重新表述为哲学家就餐问题。这个问题可以用来解释死锁和资源耗尽。 
哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。 
哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。即使没有死锁,也有可能发生资源耗尽。例如,假设规定当哲学家等待另一只餐叉超过五分钟后就放下自己手里的那一只餐叉,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生“活锁”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉。 
在实际的计算机问题中,缺乏餐叉可以类比为缺乏共享资源。一种常用的计算机技术是资源加锁,用来保证在某个时刻,资源只能被一个程序或一段代码访问。当一个程序想要使用的资源已经被另一个程序锁定,它就等待资源解锁。当多个程序涉及到加锁的资源时,在某些情况下就有可能发生死锁。例如,某个程序需要访问两个文件,当两个这样的程序各锁了一个文件,那它们都在等待对方解锁另一个文件,而这永远不会发生。

3. 信号量机制解决哲学家就餐问题

当5个哲学家进程并发执行时,某个时刻恰好每个哲学家进程都执行申请筷子,并且成功申请到第i支筷子(相当于5个哲学家同时拿起他左边的筷子), 接着他们又都执行申请右边筷子, 申请第i+1支筷子。此时每个哲学家仅拿到一支筷子, 另外一支只得无限等待下去, 引起死锁。在给出几种有效阻止死锁的方案之前,首先给出两个断言: 
(1)系统中有N个并发进程。 若规定每个进程需要申请2个某类资源, 则当系统提供N+1个同类资源时,无论采用何种方式申请资源, 一定不会发生死锁。分析:N+1个资源被N 个进程竞争, 由抽屉原理可知, 则至少存在一个进程获2个以上的同类资源。这就是前面提到的哲学家就餐问题中5个哲学家提供6支筷子时一定不会发生死锁的原因。 
(2)系统中有N个并发进程。 若规定每个进程需要申请R个某类资源, 则当系统提供K=N*(R-1)+1个同类资源时,无论采用何种方式申请使用,一定不会发生死锁。分析:在最坏的情况下,每个进程都申请到R-1个同类资源, 此时它们均阻塞。 试想若系统再追加一个同类资源, 则 N 个进程中必有一个进程获得R个资源,死锁解除。 
结合以上分析,哲学家就餐问题可以被抽象描述为:系统中有5个并发进程, 规定每个进程需要申请2个某类资源。 若系统提供5个该类资源, 在保证一定不会产生死锁的前提下,最多允许多少个进程并发执行?假设允许N个进程, 将R=2,K=5带入上述公式, 有N*(2-1)+1=5所以 N=4。也就意味着,如果在任何时刻系统最多允许4个进程并发执行, 则一定不会发生死锁。 大多数哲学家就餐问题死锁阻止算法都是基于这个结论。 
增加一个信号量,控制最多有4个进程并发执行,算法如下:

Semaphorechopstick[5]={1,1,1,1,1};//分别表示5支筷子
Semaphorefootman=4;//初始值为4最多允许4个哲学家进程同时进行
Philosopher(inti)
{while(true)
    {wait(footman);
    Think();
    Wait(chopstick[i]);//申请左筷子
    Wait(chopstick[(i+1)%5]);//申请右筷子
    Eat();
    Signal(chopstick[i]);//释放左筷子
    Signal(chopstick[(i+1)%5]);//释放右筷子
    Signal(footman);
    }
}

4.基于退回机制的哲学家进餐问题的解决

回退机制的理论依据是处理死锁基本方法中的预防死锁策略。预防死锁是指通过设置某些限制条件 ,去破坏产生死锁的 4 个必要条件中的一个或几个来防止死锁的发生. 其中“摒弃不剥夺条件”规定,当一个进程提出新的资源请求而不能立即得到满足时 ,必须释放其已经保持了的所有资源 , 即已经占有的资源可以被剥夺。根据上面的理论 ,本文解决哲学家进餐问题的基本思路是 ,让每名哲学家先去申请他左手边的筷子 ,然后再申请他右手边的筷子 ,如果右手边的筷子不空闲, 则比较当前哲学家 i 和他右手边的哲学家( i +1)%5 ,看谁取得各自左手边筷子的时间更晚, 令其回退( 即释放左手筷子, 再重新申请左手筷子), 直到此哲学家右手边的筷子空闲为止。通过设置回退机制可以确保每位哲学家都能顺利进餐。 
通过信号量来描述该算法 ,代码如下:

semaphore chopstick[ 0 …4] ={ 1 , 1 , 1 , 1 , 1};
/*chopstick [ ] :筷子信号量数组,初始值均为 1 ,表示开始时 5 根筷子都可用*/
philosopher ( i) // i : 哲学家编号, 从0到4
{ think( );//哲学家正在思考
wait( chopstick( i) );//取左侧的筷子
while (右手边筷子不空闲)
    { if (当前哲学家i比旁边哲学家( i +1)%5 晚拿到左手筷子) //%为取模运算
       {哲学家i释放左手边的筷子;
       think( );//哲学家i思考
       哲学家i重新取左侧的筷子;
       } 
else
{哲学家( i +1)%5 释放左手边的筷子;
think( );//哲学家( i +1)%5 思考
哲学家( i +1)%5 重新取左侧的筷子;
 }
}
wait( chopstick( ( i +1)%5) );//取右侧筷子
eat();//进餐
signal( chopstick( i) );//把左侧筷子放回原位
signal( chopstick( ( i +1)%5) );//把右侧筷子放回原位
}

5.用附加规则解决哲学家进餐问题

为了预防死锁的产生,我们添加一条竞争规则:所有哲学家先竞争奇数号筷子,获得后才能去竞争偶数号筷子(由于5号哲学家左右都是奇数号筷子,在本文中规定他先竞争5号筷子)。这样的话就总会有一名哲学家可以顺利获得两支筷子开始进餐。此方法的本质是通过附加的规则,让哲学家按照一定的顺序请求临界资源——筷子。这样的话,在资源分配图中就不会出现环路,破坏了死锁生的必要条件之一:“环路等待”条件,从而有效地预防了死锁的产生。接下来我们用 Java 语言来实现该刚才描述的策略。在实现代码中用五个线程表示五个哲学家的活动, 用一个逻辑型数组表示筷子的状态。 在此问题中,筷子是临界资源,必须互斥地进行访问。我们为筷子定义一个类,其中包含了表示筷子状态的逻辑。

class Chopsticks
{
/* 用 used[1]至 used[5]五个数组元素分别代表编号 1 至 5 的五支筷
子的状态 */
/* false 表示未被占用,true 表示已经被占用。 used[0]元素在程序中
未使用 */
private boolean used[]={true,false,false,false,false,false};
/* 拿起筷子的操作 */
public synchronized void takeChopstick()
{
/* 取得该线程的名称并转化为整型,用此整数来判断该哲学家应该用哪两支筷子 */
/* i 为左手边筷子编号,j 为右手边筷子编号 */
String name=Thread.currentThread().getName();
int i=Integer.parseInt(name);
/* 1~4 号哲学家使用的筷子编号是 i 和 i+1,5 号哲学家使用
的筷子编号是 5 和 1 */
int j=i==5?1:i+1;
/* 将两边筷子的编号按奇偶顺序赋给 odd,even 两个变量 */
int odd,even;
if(i%2==0){even=i;odd=j;}
else {odd=i;even=j;}
/* 首先竞争奇数号筷子 */
while(used[odd])
{
try{wait();}
catch(InterruptedException e){}
}
used[odd]=true;
/* 然后竞争偶数号筷子 */
while(used[even])
{
try{wait();}
catch(InterruptedException e){}
}
used[even]=true;
}/*放下筷子的操作 */
public synchronized void putChopstick()
{
String name=Thread.currentThread().getName();
int i=Integer.parseInt(name);
int j=i==5?1:i+1;
/* 将相应筷子的标志置为 fasle 表示使用完毕, 并且通知其
他等待线程来竞争 */
used[i]=false;
used[j]=false;
notifyAll();
}
}

当某一哲学家线程执行取得筷子方法时, 程序会根据该线程的名称来确定该线程需要使用哪两支筷子,并且分辨出哪支筷子编号是奇数,按照先奇后偶的顺序来试图取得这两支筷子。 如果这两支筷子都未被使用(即对应的数组元素值为 false),该哲学家线程即可先后取得这两支筷子进餐,否则会在竞争某支筷子失 
败后执行 wait()操作进入 Chopsticks 类实例的等待区, 直到其他的哲学家线程进餐完毕放下筷子时用 notifyAll()将其唤醒。当某一哲学家线程放下筷子时, 程序会将放下的筷子对应的数组元素值置为 false,并用 notifyAll()唤醒在等待区里的其他线程。 
接下来定义出哲学家类

class Philosopher extends Thread
{
Chopsticks chopsticks;
public Philosopher(String name,Chopsticks chopsticks)
{
/* 在构造实例时将 name 参数传给 Thread 的构造函数作为线程的名称 */
super(name);
/* 所有哲学家线程共享同一个筷子类的实例 */this.chopsticks=chopsticks;
}
public void run()
{
/* 交替地思考、拿起筷子、进餐、放下筷子 */
while(true)
{
thinking();
chopsticks.takeChopstick();
eating();
chopsticks.putChopstick();
}
}
public void thinking()
{
/* 显示字符串输出正在思考的哲学家,用线程休眠1秒钟来模拟思考时间 */
System.out.println ("Philosopher " +Thread.currentThread ().getName()+" is thinking.");
try{Thread.sleep(1000);}
catch(InterruptedException e){}
}
public void eating()
{
/* 显示字符串输出正在进餐的哲学家,并用线程休眠 1 秒钟来模拟进餐时间 */
System.out.println ("Philosopher " +Thread.currentThread ().getName()+" is eating.");
try{Thread.sleep(1000);}
catch(InterruptedException e){}
}
} 

在运行时,用Philosopher 类产生五个线程模拟五个哲学家,每个线程不停地重复执行思考、拿起筷子、进餐、放下筷子的过程。 线程的名称依次为“1”, 
“2”,“3”,“4”,“5”(字符串类型) 
主程序如下

public class Mainz
{
public static void main(String[] args)
{
/* 产生筷子类的实例 chopsticks */
Chopsticks chopsticks=new Chopsticks();
/* 用筷子类的实例作为参数, 产生五个哲学家线程并启动*/
/* 五个哲学家线程的名称为 1~5 */
new Philosopher("1",chopsticks).start();
new Philosopher("2",chopsticks).start();
new Philosopher("3",chopsticks).start();
new Philosopher("4",chopsticks).start();
new Philosopher("5",chopsticks).start();
}
}
运行后,从输出的结果可以看到五个哲学家线程交替地进行思考和进餐,互斥地使用筷子,有效地避免了死锁的发生。

6.总结

本文对哲学家进餐问题产生死锁的现象进行了分析,提出了3种解决方案, 并从在理论依据、算法设计、编程实现等方面进行了较为详细地阐述。这对学习和理解《操作系统原理》课程中的经典进程同步问题有一定的参考价值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值