#define N 5 /*哲学家数量*/
#define THINKING 0 /*哲学家处于思考状态*/
#define HUNGRY 1 /*哲学家处于饥饿状态*/
#define EATING 2 /*哲学家正在吃面*/
#define LEFT (i+N-1)%N/*当前哲学家左边的邻居*/
#define RIGHT (i+1)%N/*当前哲学家右边的邻居*/
typedef int semaphore;/*信号量类型是一种特殊的整型数据*/
semaphore mutex = 1;/*互斥量*/
semaphore s[N]; /*每个哲学家一个信号量*/
int state[N]; /*每个哲学的当前状态*/
void philosopher( int i )
{
while(TRUE){
think();
take_forks( i );
eat();
put_forks();
}
}
/*哲学家吃面问题分为四个步骤:
1、思考
2、试图拿起左右两边的叉子
3、如果成功取得两个叉子就吃面
4、放下叉子
其中对于5个哲学家的情况下,最多可以允许两个哲学家同时吃面,因此,吃面的活动是可以有两个哲学家同时进行的,所以这部分不需要互斥。但是取得叉子和放下叉子的过程很有可能出现叉子数量的同步错误问题,所以这两个过程必须使用mutex信号量来实现互斥,即:同步。
另外,如果一个哲学家试图取得叉子吃面,但是他左边或者右边或者两边的叉子都被占用的情况下,他的活动应该被阻塞(即:暂停),因此需要一个s[ i ]信号量来实现阻塞。
*/
void take_forks( int i )
{
down( &mutex );/*即: P( &mutex ),Dijkstra使用的P和V操作*/
state[ i ] = HUNGRY;/*标识 i 号哲学家当前处于饥饿状态,要求吃面*/
test( i );
up( &mutex );/*即: V( &mutex )*/
down( &s[ i ] );
}
/*down( &mutex )的作用是实现取叉子的互斥操作,也就是所谓的进入临界区,
down操作检查mutex的值,如果大于0,则递减一次mutex的值,如果等于0,则阻塞当前进程。
down是一种原语操作,整个过程绝对不会被打断。
进入临界区后,首先将自己标识饥饿状态,然后检测自己是否可以拿起叉子吃面。
接着up( &mutex ),无论是否可以拿起叉子吃面都立刻退出临界区,不能阻塞别的哲学家放下叉子,这样就避免了死锁。
down( &s[ i ] ),如果当前哲学家可以吃面,那么这个函数的执行就结束了,如果当前不能吃面,那么down操作会阻塞当前的哲学家。
*/
void put_forks( int i )
{
down( &mutex );
state[ i ] = THINKING;
test( LEFT );
test( RIGHT );
up( &mutex );
}
/*down( &mutex )进入临界区
state[ i ] = THINKING标识为思考状态
test(LEFT)尝试唤醒左边的哲学家
test(RIGHT)尝试唤醒右边的哲学家
因为当前哲学家进餐完毕后,那么左右两侧的叉子都被释放,那么左右两侧的邻居哲学家都有可能可以进餐,因此执行两个test来分别尝试唤醒两侧的邻居
up( &mutex )离开临界区
*/
void test( int i )
{
if( state[ i ] == HUNGRY && state[ LEFT ] != EATING && state[ RIGHT ] != EATING ){
state[ i ] = EATING;
up( &s[ i ] );
}
}
/*三个条件分别测试当前哲学家是否处于饥饿状态以及左右邻居哲学家当前是否不处于进餐状态。
如果三个条件都满足,那么当前哲学家便可以进餐了
up( &s[ i ] )和take_forks函数中的down( &s[ i ] )操作是对应的,其作用是:如果当前哲学家可以进餐,那么执行up操作后再执行down操作时进程就不会被阻塞,如果当前哲学家不能进餐,就不会执行up操作,如果没有执行up操作而直接执行了down操作就会阻塞当期进程。
*/
两个或两个以上进程中访问共享内存的代码称为临界区,也就是说,临界区中的代码会修改多个进程共享的内存,而这些代码会使得当多个进程同时访问共享内存的时候可能出现一些不可预知的且无法重现的错误。普遍采用的做法是:对于临界区中的代码,同一时间只允许一个进程进行操作,当操作完毕离开临界区后才允许其他进程进入临界区。