admin 管理员组

文章数量: 887021

原博客网址:哲学家就餐问题-C语言讲解
哲学家问题是操作系统中同步互斥的经典问题。通常使用信号量,管程的方式。这篇文章将会简要介绍问题的定义和类似服务生解法。并且用c语言实现解法。

问题描述:

五个哲学家围绕坐在一个圆形餐桌前,桌上放着五支筷子,每两个哲学家之间放一个筷子。哲学家的动作包括就餐和思考。思考不需要拿筷子,而就餐需要拿到两个筷子才能就餐。两个相邻的哲学家需要共享一个筷子(这个筷子就是一个共享资源)。

这种情况可能产生死锁,比如每个哲学家都拿着左手的筷子,永远都在等右边的筷子(或者相反)。

如何保证哲学家的动作有序进行,使得总有哲学家能拿到两个叉子就餐?

在哲学家问题中,哲学家代表着"线程",而叉子代表着"共享的对象"(shared object)。哲学家问题在操作系统中的真正含义是"一个线程需要两个共享的对象来完成一些工作"。

要求1:互斥(mutual exclusion)

哲学家问题的有两条互斥前提

  1. 当一个人在进餐时,别人不能偷走这个人的筷子。(难不成从这个人手上抢过去吗?)
  2. 一个筷子最多只能被一个人使用。

所以对于一个哲学家来说,当他饿了的时候,他就要检查是否有人在使用他需要的筷子,如果有,那么他等待那人使用完才能获得需要的筷子。在等待的过程中,他不会放下手上已有的筷子。如果他需要的筷子没人在使用,他就获得了指定的筷子。当他结束当前吃的动作(吃了几口就思考。。。),就放下手上所有的筷子。

上图用信号量的方式来保证了互斥条件:chopstick数组是一个信号量数组,每一信号量初始值为1(所以称为binary semaphore,但是在上图中没有表现出来),每个哲学家在调用take_chopsticks函数时其实是在调用信号量的wait函数:将信号量减一,如果信号量运算后的结果小于零则阻塞这个哲学家线程。直到获得了这个筷子的哲学家进程放下筷子,调用信号量post函数唤醒阻塞了的哲学家进程。

死锁情况

如果符合了互斥的要求,可能会出现死锁的情况:

  • 每个哲学家在同一时刻完成思考并且都拿到了自己右手边的筷子。在代码中,就是每个线程(或线程)都执行到了第四行代码。程序陷入循环等待过程中。

要求2:同步(Synchronization)

为了解决哲学家问题中的死锁情况。我们需要实现同步。

我们可以给哲学家们设置一些"协议"。比如当任意一个哲学家要进餐时检查所需筷子是否可用,如果不能够获得所有需要的筷子,就放下来。然后随机等待一段时间。时间到后,继续尝试。

忙等待

我们在上面设置的"协议"还是会出现潜在问题的:所有的哲学家同时拿起右手的筷子,放下计时同一段时间,继续拿起来。等待。所有哲学家线程starvation了(以上的情况发生的可能性看起来是很小,但是放到实际情况,发生的情况还是不算少见的)

随便设置一个协议,可能会造成效率低下。或着忙等待。

总结

  1. 用信号量方法来控制筷子满足互斥条件可能会造成死锁。
  2. 用同步来解决死锁,还是可能造成忙等待,没有死锁,但还是没有哲学家吃到饭。

所以仅仅是通过信号量互斥和同步,无法完全解决哲学家问题。

解决问题(类似服务生解法)

思路

  1. 当一个哲学家在吃饭的时候,与她相邻的哲学家不能吃饭。
  2. 同一时刻,只有一个哲学家可以检查是否满足相邻两个哲学家不在吃饭的条件。(在一些解答中用服务员这个概念来表示。餐桌上只有一个服务员,而且只有服务员有权利指派哲学家拿筷子。服务员只帮哲学家解决餐具的问题,一旦服务的哲学家拿到筷子,服务员就可以服务其他的哲学家)
    1. 如果满足,则该哲学家获取手边两个筷子就餐。其他的哲学家可以开始检查条件。
    2. 如果不满足,则将自己挂起,等待相邻的哲学家放下筷子后通知自己。再重复步骤2的检查过程。
  3. 当哲学家用餐结束。放下两支筷子,通知唤醒相邻的哲学家。

下图用信号量和互斥锁实现了思路里描述的过程

//这段代码还不能直接运行。think(),wait(),post()和eat()尚未定义。
#define N 5
#define LEFT (N+i-1)%N
#define RIGHT (i+1)%N

int state[N];/*存储哲学家状态的数组,EATING,THINKING,HUNGRY*/
semaphore mutex = 1; /*一次只能有一个哲学家操作存储哲学家状态的数组*/
semaphore p[N]={0}; /*哲学家锁,一开始每个哲学家都不占有资源,所以sema都为0,如果现在一个哲学家调用sema_wait(),会被马上阻塞*/

void take_chopsticks(int i){
  wait(&mutex);
  state[i] = HUNGRY;
  scheduler(i);//Critical Section,关键区域。
  post(&mutex);
  wait(&p[i]);
}
void take_chopsticks(int i){
  wait(&mutex);
  state[i] = HUNGRY;
  scheduler(i);
  post(&mutex);
  wait(&p[i]);
}
void scheduler(int i){
  if(state[i]==HUNGRY && state[LEFT]!= EATING && state[RIGHT]!=EATING){
    state[i]=EATING;
    post(&p[i]);
  }
}
void philosopher(int i){
  think();//也许是在思考,也许是在发呆
  take_chopsticks(i);//第i个哲学家想要拿筷子
  eat();//吃饭
  put_chopsticks();//放下筷子
}

常见疑问

  • Hungry 的意义表示你不在思考,但是因为条件不足所以也不能就餐。本质上是线程中blocked(阻塞)的状态。

  • 在section_entry中,看到captain觉得很奇怪,因为如果一旦不满足if中的条件,当前线程会被堵塞。看起来没人来恢复它。但是看到section_exit就知道这个代码设计的精妙之处了。

  • mutex是什么?保证scheduler这个critical section的同步的互斥锁。

参考

  1. 南方科技大学2019春季操作系统课Synchronization(2)
  2. 学堂在线:清华操作系统课
  3. 理解Semaphore及其用法详解
  4. 哲学家就餐问题
  5. 三种不同的方式解决‘’哲学家就餐‘’这个经典的问题

本文标签: 哲学家 语言