哲学家就餐问题

文章描述了哲学家就餐问题,即五个哲学家需要两根筷子吃饭,不当的同步可能导致死锁。文章列举了四种解决方法:1)限制只有当左右筷子都可用时才允许拿起;2)设定一个左撇子哲学家,改变拿筷子顺序;3)限制同时吃饭的哲学家人数为4;4)基于邻居状态的同步策略。这些方法通过不同机制避免了死锁,确保了哲学家能顺利进餐。
摘要由CSDN通过智能技术生成

哲学家就餐问题

该问题描述的是五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替的进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。

在这个问题中,同步的目的是确保每个哲学家都能按照规定的顺序获取筷子并进餐,而不会出现竞态条件或死锁情况正确的同步方案将保证哲学家就餐问题的正确解决,即所有哲学家都能顺利进餐,并且没有进入死锁状态

死锁出现

我们来演示一下上面的情况:放在圆桌上的筷子是临界资源,在一段时间内只允许一位哲学家的使用。为了实现对筷子的互斥使用,使用一个信号量表示一只筷子,由这五个信号量构成信号量数组。符合我们的生活:需要用一双筷子来吃饭。

 假如五位哲学家同时饥饿而各自拿起左边的筷子时,就会使五个信号量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

  • 16
    点赞
  • 83
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
哲学家进餐问题是一个经典的并发编程问题,描述了五个哲学家围坐在一张圆桌前进餐的场景,每个哲学家面前有一只碗和一只筷子,相邻哲学家之间共享一只筷子,需要用两只筷子才能进餐。如果所有哲学家同时拿起自己左边的筷子,那么他们将无法进餐,这就是死锁的情况。 解决这个问题的方法有很多,其中比较常见的是使用信号量来控制哲学家拿筷子的顺序,避免死锁的发生。 以下是一个使用 Python 实现的哲学家进餐问题的代码示例: ```python import threading class Philosopher(threading.Thread): def __init__(self, name, left_fork, right_fork): super().__init__(name=name) self.left_fork = left_fork self.right_fork = right_fork def run(self): while True: # 拿起左边的筷子 self.left_fork.acquire() # 拿起右边的筷子 if self.right_fork.acquire(blocking=False): # 如果右边的筷子可用,则开始进餐 print(f'{self.name} is eating') # 放下右边的筷子 self.right_fork.release() # 放下左边的筷子 self.left_fork.release() if __name__ == '__main__': forks = [threading.Lock() for _ in range(5)] philosophers = [Philosopher(f'Philosopher {i}', forks[i], forks[(i+1)%5]) for i in range(5)] for philosopher in philosophers: philosopher.start() ``` 在这个代码中,每个哲学家都是一个线程,每个筷子都是一个锁。每个哲学家会先拿起自己左边的筷子,然后尝试拿起右边的筷子,如果右边的筷子已经被其他哲学家拿走了,则放下左边的筷子,等待一段时间后再重新尝试。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HUN晓巫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值