经典的IPC问题

Inter-Process Communication的缩写,含义是进程间通信,是指两个进程间交换数据的过程。

哲学家进餐问题

概述

img

  • 哲学家进餐/思考
  • 进餐需要两把叉子
  • 每次拿一把叉子
  • 如何预防死锁

问题简介

哲学家的生活包括两个不同的阶段:吃饭和思考

当一个哲学家觉得饿时,他就试图去取他左边和右边的叉子,每次拿一把,但是部分次序,如果成功地获得了两把叉子,他就吃一会儿,然后放下叉子继续思考。

关键的问题就是:为每个哲学家写一段程序来描述其行为而且不能思索,你可以做到吗?

一种不正确的解法

#define N 5		//哲学家的数目
void philosopher(int i) {		//i是哲学家的编号,从0到4
    while (true) {
        think();				//哲学家正在思考
        take_fork(i);			//取左边的叉子
        take_fork((i + 1) % N);	//取右面叉子:%表示取模运算
        eat();					//空心粉味道不错
        put_fork(i);			//把左面叉子放回桌子
        put_fork((i + 1) % N)	//把右面叉子放回桌子
    }
}

但是当所有哲学家同时拿起左边的叉子,无法得到右边的叉子——死锁

程序稍作修改:所有的哲学家都同时拿起左叉,看到右叉不可用,又都放下左叉,等一会儿,又同时拿起左叉,如此这般,永远重复。对于这种情况,即所有的程序都在无限期地运行,但是都无法取得任何进展,就成为饥饿(starvation)。

对上例算法可做一点改进,它既不会死锁也不会饥饿:即,使用一个二进制信号量对五个think之后的语句进行保护。在开始拿叉子之前,哲学家先对信号量mutex执行down操作。在放回叉子后,他要对mutex执行up操作。

从理论上讲,该解法是可行的。但是从实力角度来看,有性能上的缺陷:任意时刻只能有一个哲学家进餐。而五把叉子实际上允许有两位哲学将同时进餐。

解法

#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];                     /* 每位哲学家一个信号量 */

/* i: 哲学家编号,从0到N-1 */
void philosopher(int i) {
    while (TRUE) {                  /* 无限循环 */
        think();                    /* 哲学家思考 */
        take_forks(i);              /* 需要两个叉子, */
        eat();                      /* 哲学家进餐 */
        put_forks(i);               /* 将叉子放回到桌子上 */
    }
}

void take_forks(int i) {
    down(&mutex);                   /* 进入临界区 */
    state[i] = HUNGRY;              /* 记录哲学家i处于饥饿状态 */
    test(i);                        /* 尝试获取两把叉子 */
    up(&mutex);                     /* 退出临界区 */
    down(&s[i]);                    /* 如果得不到需要的叉子则阻塞 */
}

void put_forks(i) {
    down(&mutex);                   /* 进入临界区 */
    state[i] = THINKING;            /* 哲学家进餐完毕 */
    test(LEFT);                     /* 测试左邻是否可以吃 */
    test(RIGHT);                    /* 测试右邻是否可以吃 */
    up(&mutex);                     /* 离开临界区 */
}

void test(i) {
    //我是饥饿的但是左右都不再吃的时候
    if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING) {
        state[i] = EATING;
        up(&s[i]);
    }
}

上面给出的解法是没有死锁的,而且对于任意多位哲学家的情况都能获得最大的并行度。它使用一个数组state来记录哲学家是在吃饭、思考还是饿了。一个哲学家只有在两个邻座都不在进餐时,才允许转换到进餐状态。哲学家i的邻居是由宏LEFTRIGHT定义。

该程序使用了一个信号量数组,每个信号量对应于一位哲学家,这样,所需的叉子被占用时,饥饿的哲学家就可以被阻塞。注意每个进程将历程philosopher作为朱代码运行,而其他例程,如take_forksput_forkstest都只是普通的例程,而不是单独的进程。

读者/写者问题

另一个著名的问题是读者—写者问题,它建模了对数据库的访问。

例如,设想一个飞机定票系统,其中有许多竞争的进程试图读写其中的数据。多个进程同时读是可以接受的,但如果一个进程正在更新数据库,则所有其他进程都不能访问数据库,即使读操作也不行。这里的问题是:如何对读者和写者进行编程?

进程A操作 进程B操作 是否允许
允许
互斥
互斥
typedef int semaphore;
semaphore mutex = 1;    	/* 控制对RC的访问 */
semaphore db = 1;       	/* 控制对数据库的访问 */
int rc = 0;        			/* 正在读或想要读的进程数 */

void reader(void) {
    while (TRUE) {        	/* 无限循环 */
        down(&mutex);    	/* 排斥对RC的访问*/
        rc = rc + 1;    	/* 又多了一个读者 */
        /*如果这是第一个读者,那么......*/
        if (rc == 1)
            //只要有一个读者在读书编者就不能编书
            //当前有进程在读取数据库
            down(&db);
        up(&mutex);    		/*恢复对RC的访问*/
        read_data_base(); 	/*访问数据*/
        down(&mutex);    	/*排斥对RC的访问*/
        rc = rc - 1;    	/*读者又少了一个*/
        /*如果这是最后一个读者,那么......*/
        if (rc == 0)
            up(&db);
        use_data_read();    /*非临界区操作*/
    }
}

void writer(void) {
    while (TRUE) {
        think_up_data();    /*非临界区操作*/
        down(&db);    		/*排斥访问*/
        write_data_base();  /*修改数据*/
        up(&db);        	/*恢复访问*/
    }
}

第一个读者对信号量db执行DOWN。随后的读者给计数器rc加1。当读者离开时,它们递减这个计数器,而最后一个读者则对db执行UP这样就允许一个阻塞的写者可以访问数据库

设想当一个读者在使用数据库时,另一个读者也来访问数据库,由于同时允许多个读者同时进行读操作,所以第二个读者也被允许进入,同理第三个及随后更多的读者都被允许进入。

posted @ 2020-04-11 15:27  我係死肥宅  阅读(744)  评论(0编辑  收藏  举报