哲学家就餐问题
该问题描述的是五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替的进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。
在这个问题中,同步的目的是确保每个哲学家都能按照规定的顺序获取筷子并进餐,而不会出现竞态条件或死锁情况。正确的同步方案将保证哲学家就餐问题的正确解决,即所有哲学家都能顺利进餐,并且没有进入死锁状态。
死锁出现
我们来演示一下上面的情况:放在圆桌上的筷子是临界资源,在一段时间内只允许一位哲学家的使用。为了实现对筷子的互斥使用,使用一个信号量表示一只筷子,由这五个信号量构成信号量数组。符合我们的生活:需要用一双筷子来吃饭。
假如五位哲学家同时饥饿而各自拿起左边的筷子时,就会使五个信号量chopstick均为0;而当他们再试图去拿右边的筷子时,都将因无筷子可拿造成无限的等待,产生死锁。
代码
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <pthread.h>
#include <errno.h>
#include <math.h>
//筷子作为mutex
pthread_mutex_t chopstick[6] ;
void *eat_think(void *arg)
{
char phi = *(char *)arg;
int left,right; //左右筷子的编号
switch (phi){
case 'A':
left = 5;
right = 1;
break;
case 'B':
left = 1;
right = 2;
break;
case 'C':
left = 2;
right = 3;
break;
case 'D':
left = 3;
right = 4;
break;
case 'E':
left = 4;
right = 5;
break;
}
int i;
for(;;){
usleep(3); //思考
pthread_mutex_lock(&chopstick[left]); //拿起左手的筷子
printf("贤者 %c 拿起筷子 %d\n", phi, left);
/* if (pthread_mutex_trylock(&chopstick[right]) == EBUSY){ //拿起右手的筷子
pthread_mutex_unlock(&chopstick[left]); //如果右边筷子被拿走放下左手的筷子
continue;
}*/
pthread_mutex_lock(&chopstick[right]);
//拿起右手的筷子,如果想观察死锁,把上一句if注释掉,再把这一句的注释去掉
printf("贤者 %c 拿起筷子 %d\n", phi, right);
printf("贤者 %c 快乐干饭.\n",phi);
usleep(3); //吃饭
pthread_mutex_unlock(&chopstick[left]); //放下左手的筷子
printf("贤者 %c 放下筷子 %d\n", phi, left);
pthread_mutex_unlock(&chopstick[right]); //放下左手的筷子
printf("贤者 %c 放下筷子 %d\n", phi, right);
}
}
int main(){
pthread_t A,B,C,D,E; //5个哲学家
int i;
for (i = 0; i < 5; i++)
pthread_mutex_init(&chopstick[i],NULL);
pthread_create(&A,NULL, eat_think, "A");
pthread_create(&B,NULL, eat_think, "B");
pthread_create(&C,NULL, eat_think, "C");
pthread_create(&D,NULL, eat_think, "D");
pthread_create(&E,NULL, eat_think, "E");
pthread_join(A,NULL);
pthread_join(B,NULL);
pthread_join(C,NULL);
pthread_join(D,NULL);
pthread_join(E,NULL);
return 0;
}
下面是运行产生死锁程序的截图。(b.c)
方法一:
对上述算法进行改进,限制仅当哲学家左右的两只筷子都可用时,才允许他拿起筷子进餐。利用信号量的保护机制实现。利用信号量的保护机制实现的思想是通过记录型信号量mutex对取左侧和右侧筷子的操作进行保护,使之成为一个原子操作,这样可以防止死锁的出现。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/wait.h>
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
//申请一个资源
int wait_1chop(int no,int semid)
{
//资源减1
struct sembuf sb = {no,-1,0};
int ret;
ret = semop(semid,&sb,1);
if(ret < 0) {
ERR_EXIT("semop");
}
return ret;
}
// 释放一个资源
int free_1chop(int no,int semid)
{
//资源加1
struct sembuf sb = {no,1,0};
int ret;
ret = semop(semid,&sb,1);
if(ret < 0) {
ERR_EXIT("semop");
}
return ret;
}
//筷子是一个临界资源
#define DELAY (rand() % 5 + 1)
//相当于P操作
//第一个参数是筷子编号
//第二个参数是信号量编号
void wait_for_2chop(int no,int semid)
{
//哲学家左边的筷子编号和哲学家编号是一样的
int left = no;
//右边的筷子
int right = (no + 1) % 5;
//筷子值是两个
//操作的是两个信号量,即两种资源都满足,才进行操作
struct sembuf buf[2] = {
{left,-1,0},
{right,-1,0}
};
//信号集中有5个信号量,只是对其中的资源sembuf进行操作
semop(semid,buf,2);
}
//相当于V操作 ,释放筷子
void free_2chop(int no,int semid)
{
int left = no;
int right = (no + 1) % 5;
struct sembuf buf[2] = {
{left,1,0},
{right,1,0}
};
semop(semid,buf,2);
}
//哲学家要做的事
void philosophere(int no,int semid)
{
srand(getpid());
for(;;)
{
#if 1
//当两只筷子都可用的时候,哲学家才能进餐
printf("%d is thinking\n",no); //思考中
sleep(DELAY);
printf("%d is hungry\n",no); //饥饿
wait_for_2chop(no,semid); //拿到两只筷子才能吃饭
printf("%d is eating\n",no); //进餐
sleep(DELAY);
free_2chop(no,semid); //释放两只筷子
#else
//可能会造成死锁
int left = no;
int right = (no + 1) % 5;
printf("%d is thinking\n",no); //思考中
sleep(DELAY);
printf("%d is hungry\n",no); //饥饿
wait_1chop(left,semid); //拿起左筷子(只要有一个资源就申请)
sleep(DELAY);
wait_1chop(right,semid); //拿起右筷子
printf("%d is eating\n",no); //进餐
sleep(DELAY);
free_1chop(left,semid); //释放左筷子
free_1chop(right,semid); //释放右筷子
#endif
}
}
int main(int argc,char *argv[])
{
int semid;
//创建信号量集,其中有5个信号量
semid = semget(IPC_PRIVATE,5,IPC_CREAT | 0666);
if(semid < 0) {
ERR_EXIT("semid");
}
union semun su;
su.val = 1;
int i;
for(i = 0;i < 5; ++i) {
//第二个参数也是索引
semctl(semid,i,SETVAL,su);
}
//创建4个子进程
int num = 0;
pid_t pid;
for(i = 1;i < 5;++i)
{
pid = fork();
if(pid < 0)
{
ERR_EXIT("fork");
}
if(0 == pid) //子进程
{
num = i;
break;
}
}
//哲学家要做的事情
philosophere(num,semid);
return 0;
}
(代码可以一直运行)
参考:操作系统第六次实验报告——使用信号量解决哲学家进餐问题 - 星野妙 - 博客园 (cnblogs.com)
方法二:
找到一个左撇子哲学家
这种方法是让其中一个哲学家和其他哲学家拿筷子的顺序和其他哲学家不一样。比如其他人都是先拿右手边再拿左手边,而这个左撇子哲学家则先拿左手边再拿右手边。而哪一位哲学家被选为左撇子并不重要,因为桌子是圆的,所以我们就选择0号哲学家为左撇子。
(c.c)
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#define N 5 // 哲学家的数量
sem_t forks[N]; // 信号量数组,表示5个筷子
pthread_t philosophers[N]; // 哲学家线程
void think(int id) {
printf("哲学家 %d 正在思考\n", id);
sleep(1); // 假设思考1秒钟
}
void eat(int id) {
printf("哲学家 %d 正在干饭\n", id);
sleep(1); // 假设吃饭1秒钟
}
void *philosopher(void *num) {
int id = (int)num;
while(1) {
think(id);
if(id == 0) { // 左撇子哲学家
sem_wait(&forks[(id+1)%N]); // 先拿起右边的筷子
printf("哲学家 %d 拿起 筷子 %d\n", id, (id+1)%N);
sem_wait(&forks[id]); // 再拿起左边的筷子
printf("哲学家 %d 拿起 筷子 %d\n", id, id);
} else { // 右撇子哲学家
sem_wait(&forks[id]); // 先拿起左边的筷子
printf("哲学家 %d 拿起 筷子 %d\n", id, id);
sem_wait(&forks[(id+1)%N]); // 再拿起右边的筷子
printf("哲学家 %d 拿起 筷子 %d\n", id, (id+1)%N);
}
eat(id);
sem_post(&forks[id]); // 放下左边的筷子
printf("哲学家 %d 放下 筷子 %d\n", id, id);
sem_post(&forks[(id+1)%N]); // 放下右边的筷子
printf("哲学家 %d 放下 筷子 %d\n", id, (id+1)%N);
}
}
int main() {
int i;
for( i = 0; i < N; i++) {
sem_init(&forks[i], 0, 1); // 初始化每个信号量为1,表示每个筷子都可用
}
for( i = 0; i < N; i++) {
pthread_create(&philosophers[i], NULL, philosopher, (void*)i); // 创建哲学家线程
}
for( i = 0; i < N; i++) {
pthread_join(philosophers[i], NULL); // 等待所有哲学家线程结束
}
return 0;
}
运行结果:
方法三:
限制吃饭的哲学家人数
很简单的一种方法,就是在一个时间点,只能有最多4个哲学家开始吃饭。4个哲学家分5只筷子,则永远不会发生死锁。(d.c)
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#define N 5 // 哲学家的数量
sem_t forks[N]; // 信号量数组,表示5个筷子
pthread_t philosophers[N]; // 哲学家线程
sem_t maxDiners; // 最多4个哲学家可以同时进餐
void think(int id) {
printf("哲学家 %d 正在思考人生\n", id);
sleep(1); // 假设思考1秒钟
}
void eat(int id) {
printf("哲学家 %d 在猛烈干饭\n", id);
sleep(1); // 假设吃饭1秒钟
}
void *philosopher(void *num) {
int id = (int)num;
while(1) {
think(id);
sem_wait(&maxDiners); // 获取吃饭许可
sem_wait(&forks[id]); // 先拿起左边的筷子
printf("哲学家 %d 拿起左边的筷子 %d\n", id, id);
sem_wait(&forks[(id+1)%N]); // 再拿起右边的筷子
printf("哲学家 %d 拿起右边的筷子 %d\n", id, (id+1)%N);
eat(id);
sem_post(&forks[id]); // 放下左边的筷子
printf("哲学家 %d 放下左边的筷子 %d\n", id, id);
sem_post(&forks[(id+1)%N]); // 放下右边的筷子
printf("哲学家 %d 放下右边的筷子 %d\n", id, (id+1)%N);
sem_post(&maxDiners); // 释放吃饭许可
}
}
int main() {
sem_init(&maxDiners, 0, 4); // 初始化maxDiners信号量为4,表示最多有4个哲学家可以同时吃饭
int i;
for( i = 0; i < N; i++) {
sem_init(&forks[i], 0, 1); // 初始化每个信号量为1,表示每个筷子都可用
}
for( i = 0; i < N; i++) {
pthread_create(&philosophers[i], NULL, philosopher, (void*)i); // 创建哲学家线程
}
for( i = 0; i < N; i++) {
pthread_join(philosophers[i], NULL); // 等待所有哲学家线程结束
}
return 0;
}
运行结果:
方法四:
一个哲学家只有在两个邻居都不在进餐时才允许进入到进餐状态。(和方法一相似)
在 pickup_forks 函数中,哲学家试图拿起筷子。 首先,他将自己的状态设置为“饥饿”,然后检查他的两个邻居是否都在吃饭。 如果他的邻居都不在吃饭,他就开始吃饭,否则他就等待。
在 return_forks 函数中,哲学家吃完饭后放下筷子并开始思考。 此时,他会检查他的两个邻居是否饥饿并等待筷子,如果是,就唤醒他们。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define N 5 // 哲学家的数量
#define THINKING 0
#define HUNGRY 1
#define EATING 2
int state[N]; // 哲学家的状态
pthread_mutex_t mutex; // 互斥量用于保护哲学家的状态
pthread_cond_t cond[N]; // 条件变量用于等待两个邻居都不在吃饭
void think(int id) {
printf("哲学家 %d 正在怀疑人生\n", id);
sleep(1); // 假设思考1秒钟
}
void eat(int id) {
printf("哲学家 %d 正在快乐干饭\n", id);
sleep(1); // 假设吃饭1秒钟
}
void test(int id) {
if (state[id] == HUNGRY && state[(id+4)%N] != EATING && state[(id+1)%N] != EATING) {
state[id] = EATING;
pthread_cond_signal(&cond[id]);
}
}
void pickup_forks(int id) {
pthread_mutex_lock(&mutex);
state[id] = HUNGRY;
printf("哲学家 %d 处于饥饿并且准备拿起筷子\n", id);
test(id);
if (state[id] != EATING) {
pthread_cond_wait(&cond[id], &mutex);
}
else {
printf("哲学家 %d 拿起一双筷子\n", id);
}
pthread_mutex_unlock(&mutex);
}
void return_forks(int id) {
pthread_mutex_lock(&mutex);
state[id] = THINKING;
printf("哲学家 %d 放下筷子并且思考\n", id);
test((id+4)%N); // 尝试让左边的邻居吃饭
test((id+1)%N); // 尝试让右边的邻居吃饭
pthread_mutex_unlock(&mutex);
}
void* philosopher(void* num) {
int id = (int)num;
while (1) {
think(id);
pickup_forks(id);
eat(id);
return_forks(id);
}
return NULL;
}
int main() {
pthread_t philosophers[N];
pthread_mutex_init(&mutex, NULL);
int i ;
for ( i = 0; i < N; i++) {
pthread_cond_init(&cond[i], NULL);
state[i] = THINKING;
}
for ( i = 0; i < N; i++) {
pthread_create(&philosophers[i], NULL, philosopher, (void*)i);
}
for ( i = 0; i < N; i++) {
pthread_join(philosophers[i], NULL);
}
return 0;
}
运行结果:
上面运行的步骤(虚拟机linux环境下)(都是c代码)
步骤一:
gcc f.c -o f -lpthread -lrt
步骤二:
./f