五个哲学家就餐问题

哲学家就餐问题

1 描述

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

在这里插入图片描述

附:进程互斥与同步,死锁基本知识

在多道程序环境下,进程有异步和同步两种并发执行方式。异步执行是指运行中的各进程在操作系统的调度下以不可预知的速度向前推进。 异步执行的进程大多没有时序要求,不存在“执行结果与语句的特定执行顺序有关”的条件竞争。然而存在一类协作进程,“保证数据的一致性” 的前提要求它们必须按某种特定顺序执行,并且遵守如下两种限制。

  1. R1(顺序化执行):进程A 的eventA事件必须发生在进程B的eventB事件之前;
  2. R2(互斥执行):进程 A的eventA事件与进程B的eventB事件不能同时发生。把上述限制下多进程的运行状态叫作进程的同步执行。进程同步执行时因存在着明显的执行上的时序要求而相互等待。如果说进程异步是进程并发执行的自然结果,那么进程同步则需要程序员通过准确嵌入一些诸如加解锁来确保实现。

信号量无疑是一个较为理想的同步工具。它最早由荷兰科学家Dijkstra于1965年提出,该工具具有如下三个优点:
3. 仅需要两个基本操作即可完成进程的同步和互斥,而且两个原子操作代码简洁高效, 易于扩充;
4. 精心设计的信号量对象类似一条条“触发器”规则,加上信号量机制的强制作用可以帮助程序员少犯错误;
5. 信号量已在很多系统中实现,解决方案中有意识地选用信号量无疑将使进程更“瘦身”,运行更高效。
信号量技术的引入是对早期忙等型(busywaiting)进程控制变量是个巨大的提升,但在使用过程中仍然存在不少缺点:

  • 一是不能随时读取信号量的值, 必要时须重复定义一个跟踪信号量值的普通变量,
  • 二是程序员对信号量的PV操作的正确使用与否没有任何控制和保证(后来引入管程和条件变量,PV操作完全由编译器而非 程序员安排),不合理地使用将导致进程饥饿甚至死锁。

死锁应尽可能阻止,系统死锁导致诸进程将进入无法向前推进的僵持状态, 除非借助于外力。死锁的原因除了系统资源偏少之外,更多的是进程推进速度不当, 或者说进程申请和释放信号量的顺序不合理所致,毕竟系统提供的资源是有限的。以哲学家就餐问题为例,若派发给每位哲学家一双筷子(更准确地说,6支就足够), 则一定不会死锁。事实上,若信号量的PV操作顺序处置得当,5支筷子同样也可以保证不会发生死锁。

2 分析

  1. 关系分析。 5名哲学家与左右邻居对其中间筷子的访问是互斥关系。

  2. 整理思路。 显然这里有五个进程。本题的关键是如何让一个哲学家拿到左右两个筷子而不造成死锁或者饥饿现象。那么解决方法有两个 ,一个是让他们同时拿两个筷子;二是对每个哲学家的动作制定规则,避免饥饿或者死锁现象的发生。

  3. 信号量设置。 定义互斥信号量数组Ch0PstiCk[5] = {l, 1, 1, 1, 1}用于对5个筷子的互斥访问。对哲学家按顺序从0~4编号,哲学家i左边的筷子的编号为i,哲学家右边的筷子的编号为(i+l)%5

semaphore chopstick[5] = {1,1,1,1,1}; //定义信号量数组chopstick[5],并初始化
Pi(){  //i号哲学家的进程
    while(1){
        P(chopstick[i]); //取左边筷子
        P(chopstick[(i+1)%5]);  //取右边篌子
        eat;  
        V(chopstick[i]); //放回左边筷子
        V(chopstick[(i+1)%5]);  //放回右边筷子
        think;  
    } 
}

该算法存在以下问题:当五个哲学家都想要进餐,分别拿起他们左边筷子的时候(都恰好执行完wait(chopstick[i]);)筷子已经被拿光了,等到他们再想拿右边的筷子的时候(执行 wait(chopstick[(i+l)%5]);)就全被阻塞了,这就出现了死锁。

为了防止死锁的发生,可以对哲学家进程施加一些限制条件,比如:

  • 至多允许四个哲学家同时进餐;
  • 仅当一个哲学家左右两边的筷子都可用时才允许他抓起筷子;
  • 对哲学家顺序编号,要求奇数号哲学家先抓左边的筷子,然后再转他右边的筷子,而偶数号哲学家刚好相反。

3 解法

3.1 解法一

假设当一个哲学家左右两边的筷子都可用时,才允许他抓起筷子。

semaphore chopstick[5] = {1,1,1,1,1}; //初始化信号量
semaphore mutex=l;  //设置取筷子的信号量

Pi(){ //i号哲学家的进程
    while(1){
        P(mutex); //在取筷子前获得互斥量,一次只能由一个哲学家取筷子
        P(chopstick[i]) ; //取左边筷子
        P(chopstick[(i+1)%5]);  //取右边筷子
        V(mutex); //释放取筷子的信号量
        eat;  
        V(chopstick[i]);  //放回左边筷子
        V(chopstick[(i+1)%5]);  //放回右边筷子
        think;  
    }
}

3.2 解法二

当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个进程并发执行,算法如下:

semaphore chopstick[5] = {1,1,1,1,1}; //初始化信号量
semaphore eating = 4;  //至多只允许四个哲学家可以同时进餐

Pi(){ //i号哲学家的进程
    while(1){
        think; 
        P(eating); //请求进餐,若是第五个则挨饿
        P(chopstick[i]); //取左边筷子
        P(chopstick[(i+1)%5]) ;  //取右边筷子
        eat;  
        V(chopstick[(i+1)%5]) ;  //放回右边筷子
        V(chopstick[i]) ;  //放回左边筷子
        V(eating); //释放信号量给其他挨饿的哲学家
    }
}

3.3 解法三

规定奇数号哲学家先拿他左边的筷子,然后在去拿右边的筷子;而偶数号哲学家则相反。按此规定,将是1、2号哲学家竞争1号筷子;3、4号哲学家竞争3号筷子。

即5位哲学家都先竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一位哲学家能够获得两只筷子而进餐。

semaphore chopstick[5] = {1,1,1,1,1}; //初始化信号量

Pi(){ //i号哲学家的进程
    while(1){
        think; 
        if(i%2==0){
            P(chopstick[(i+1)%5]) ;  //取右边筷子
            P(chopstick[i]); //取左边筷子
            eat;  
            V(chopstick[(i+1)%5]) ;  //放回右边筷子
            V(chopstick[i]) ;  //放回左边筷子
        }else{           //奇数哲学家,先左后右
            P(chopstick[i]); //取左边筷子
            P(chopstick[(i+1)%5]) ;  //取右边筷子
            V(mutex); //释放互斥量
            eat;  
            V(chopstick[i]) ;  //放回左边筷子
            V(chopstick[(i+1)%5]) ;  //放回右边筷子
        
        }
    }
}

3.4 解法四

采用AND型信号量机制来解决,即要求每个哲学家先获得两个临界资源(筷子)后方能进餐。

semaphore chopstick[5] = {1,1,1,1,1}; //初始化信号量
semaphore mutex = 1;  //设置取筷子的信号量

Pi(){
    while(1){
        think; 
        P(chopstick[i],chopstick[(i+1)%5]); 
        eat;  
        V(chopstick[i],chopstick[(i+1)%5]); 
    }
}

3.5方案五:

下面的解法不仅正确,而且能获得最大的并行度。其中使用一个数组state来跟踪一个哲学家是在吃饭、思考还是正在试图拿叉子:一个哲学家只有在两个邻居都不在进餐时才允许进入到进餐状态。第i位哲学家的邻居由宏LEFT和RIGHT定义。

哲学家进餐问题的解决方案使用了一个信号量数组,每个信号量分别对应一个哲学家,这样,当所需的叉子被占用时,想进餐的哲学家可以阻塞。注意,每个进程将过程philosopher作为主代码运行。

#define N           5               /* 哲学家数目 */
#define LEFT        (i+N-1)%N       /* i的左邻号码 */
#define RIGHT       (i+1)%N         /* i的右邻号码 */

#define THINKING    0               /* 哲学家正在思考 */
#define HUNGRY      1               /* 哲学家想取得叉子 */
#define EATING      2               /* 哲学家正在吃面 */

typedef int semaphore;
int state[N];                       /* 记录每个人状态的数组 */
semaphore mutex=1;                  /* 临界区互斥 */
semaphore s[N];                     /* 每个哲学家一个信号量 */

void philosopher(int i)             /* i:哲学家号码,从0到N-1 */
{
    while(TRUE)
    {   
        think();                    /* 哲学家正在思考 */
        take_forks(i);              /* 需要两把叉子,或者阻塞 */
        eat();                      /* 进餐 */
        put_forks(i);               /* 把两把叉子同时放回桌子 */
    }
}

void take_forks(int i)              /* i:哲学家号码,从0到N-1 */
{    
    down(&mutex);                   /* 进人临界区 */
    state[i]=HUNGRY;                /* 记录下哲学家i饥饿的事实 */
    test(i);                        /* 试图得到两把叉子 */
    down(&s[i]);                    /* 如果得不到叉子就阻塞 */
}

void put_forks(i)                   /* i:哲学家号码,从0到N-1 */
{
    down(&mutex);                   /* 进人临界区 */
    state[i]=THINKING;              /* 哲学家进餐结束 */
    test(LEFT);                     /* 看一下左邻居现在是否能进餐 */
    test(RIGHT);                    /* 看一下右邻居现在是否能进餐 */
    up(&mutex);                     /* 离开临界区 */
}

void test(i)                        /* i:哲学家号码,从0到N-1 */
{
    if(state[i]==HUNGRY && state[LEFT]!=EATING&&state[RIGHT]!=EATING){
        state[i]=EATING;
        up(&s[i]);
    }
}

4 拓展

5个哲学家问题本质上是解决并发程序中的死锁和饥饿,可以将推广为更一般的n个进程,m个共享资源的问题。

教材原文:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章