哲学家进餐问题实验报告_【心得篇】经典同步问题

c253ede910aea96c966c0975bd6044cb.png

问题提出

我不止一次地强调,计算机的资源是有限的,但人们会用大量任务压榨计算机。遗憾的是,对于那些十分有限的资源,这些任务不能同时调用它们,需要协调。(打个无伤大雅的比方,就像卫生间坑位有限,需要协调一样,你肯定无法想象多人共用一个坑位吧)。总而言之,协调好资源就是一种同步(让大家都把自己的“时钟”调到资源那儿)。

问题解决

如果卫生间没有坑位了,怎么办?等呗。如果等到坑位,就可以使用了(注意要把门关上)。最后,使用完毕要冲掉,然后释放坑位资源。

在代码层面,其实所谓的“P/V”操作就是这里的“等待”(wait())和“释放”(signal())。

在使用前,要等待有空闲资源,就是P()一下,或者说wait()一下;当等来资源的时候,就进入了程序的“临界区”,独自使用资源;使用完资源之后,就得释放资源,就是V()一下,或者说signal()一下(就像高铁上的灯指示一下,更新一下可用资源数量)。

经典案例

哲学家进餐问题。设想一张桌子上有5位哲学家,他们只做两件事:拿起筷子吃饭或者放下筷子思考,他们不会一边吃饭一边思考。有趣之处在于,桌子上共有5根筷子,与哲学家一起排成一个环,每位哲学家双手边都各有一根筷子。如下图:

2e4f038f682397bd0748c6d5c0ae99d7.png
新学的灵魂手绘(小圆头是哲学家,红线是筷子)

怎样用代码描述哲学家吃饭和思考的过程而不引起冲突{同时使用一根筷子}呢?让我们试着解决这个同步问题。

  1. 共用的资源是什么?
    很明显是筷子啊。但是这跟日常上厕所的问题不太一样,平时有个坑就能去,但在这里,只能用双手边的筷子,不能拿别人那儿的筷子。(可以拿邻居的。。)所以事实上,筷子和筷子之间是不同的,我们要给每根筷子一个信号量(就是一个数,类比卫生间门口有个指示坑位的数字),这个信号量的初始值是1,因为每根筷子都是独一无二的。
  2. 有哪些活动?相关约束是什么?
    吃饭和思考,吃饭前要拿筷子,吃完之后放下筷子;思考前要放下筷子,思考后等着下一顿饭。

于是我们就很快写出如下代码:

semaphore chopsticks[5]={1,1,1,1,1};

void Philosopher_i(){//i号哲学家的进程
    do{
        wait(chopsticks[i]);//等左手边的筷子
        wait(chopsticks[(i+1)%5]);//等右手边的筷子
        eat();//吃饭
        signal(chopsticks[(i+1)%5]);//放下右手的筷子
        signal(chopsticks[i]);//放下左手的筷子
        think();//思考
    }while(true);//像个没有感情的机器人继续循环
}

这段代码cover了前面讲过的所有要点,并且能够保证不产生冲突。

但是!你发现没有,在这种同步机制下,可能出现哲学家们没法吃饭、没法思考、面面相觑的情况。试想某个时刻,所有人都拿起了自己左手边的筷子,桌子上没有筷子能拿了,也没有人会放下,而每个人此时只有一根筷子,没法吃饭也没法思考——大家都懵了!“太默契了吧小老弟”

这种情况不算冲突,但是属于异常情形(术语叫“死锁”),所以要避免。如何避免呢?有很多方法,以下介绍最简单的一种。

我们先想想,什么时候会出现上面的死锁?在刚开始的时候,大家同时运行代码中的第一行"wait(chopsticks[i])"把左手边的筷子拿了起来,这就产生了死锁。那么,如果限定某一时刻桌子上只有一位哲学家能拿筷子,问题岂不就解决了?对的。我们不妨把这种拿筷子的权限抽象成一个单位的独占资源,这样就能很快写出如下代码:

semaphore chopsticks[5]={1,1,1,1,1};
semaphore mutex=1;//互斥量(抽象的独占资源)
void Philosopher_i(){//i号哲学家的进程
    do{
        wait(mutex);//等到只有我能拿筷子
        wait(chopsticks[i]);//等左手边的筷子
        wait(chopsticks[(i+1)%5]);//等右手边的筷子
        signal(mutex);//我拿完筷子了,让别人拿吧
        eat();//吃饭
        signal(chopsticks[(i+1)%5]);//放下右手的筷子
        signal(chopsticks[i]);//放下左手的筷子
        think();//思考
    }while(true);//像个没有感情的机器人继续循环
}

这种解决办法还算漂亮,但并不完美,因为那么大的桌子,每次只让一个人拿筷子,效率也太低了吧,事实上相隔较远的哲学家并不会产生冲突,可这样子处理也把他们给限制了。有没有更优美的解决方案呢?

当然有。不知道大家记不记得以前学组合数学的时候有个鸽巢原理(或者叫抽屉原理),就是说n+1只相同的袜子放在n个抽屉里,则至少有一个抽屉里能凑成一双。很好理解吧,考虑最坏情况——即每个抽屉里只有一只袜子,这样一共n只袜子,还有一只袜子不管放在哪个抽屉里都能凑成一双了。

如果我们把筷子当成袜子,最多几个人同时拿这5只筷子的时候,至少有一个人能拿到一双筷子呢?很显然答案是4。我们就把这个4当作拿筷子的名额,很容易写出代码如下:

semaphore chopsticks[5]={1,1,1,1,1};
semaphore mutex=4;//互斥量(拿筷子的名额)
void Philosopher_i(){//i号哲学家的进程
    do{
        wait(mutex);//等我抢到一个拿筷子的名额
        wait(chopsticks[i]);//等左手边的筷子
        wait(chopsticks[(i+1)%5]);//等右手边的筷子
        signal(mutex);//我拿完筷子了,释放一个名额
        eat();//吃饭
        signal(chopsticks[(i+1)%5]);//放下右手的筷子
        signal(chopsticks[i]);//放下左手的筷子
        think();//思考
    }while(true);//像个没有感情的机器人继续循环
}

其实这段代码和前一段代码的差别只有第二行,但在实际运行的时候,情况可大不相同。如果哲学家人数增多,显然后面的程序效率更高。

总结

为了更好地实现共享,必须解决“同步”和“互斥”这两种关系。以上案例展示了经典的处理方案,如果用更复杂的信号量机制,也可以有其他处理办法,但超出了目前考研的要求。感兴趣的朋友可以找来相关论文和教材进行研究。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
哲学家进餐问题经典的并发编程问题,其场景为五位哲学家围坐在一个圆桌旁,每位哲学家需要交替地进行思考和进餐,而进餐需要使用两个相邻的叉子,因此如果每位哲学家都拿起自己左边的叉子,就会导致死锁。 解决这个问题的方法之一就是利用信号量机制。我们可以为每个叉子都创建一个信号量,表示该叉子是否可用。在每位哲学家进行进餐时,先判断其左右两个叉子是否都可用,如果都可用,则分别将左右两个叉子的信号量减1,表示占用了这两个叉子,然后开始进餐。进餐完成后,将左右两个叉子的信号量加1,表示释放了这两个叉子。 以下是使用信号量机制解决哲学家进餐问题实验报告: 一、实验目的 掌握信号量机制在解决并发编程问题中的应用,理解哲学家进餐问题的场景和解决方法。 二、实验环境 操作系统:Linux 编程语言:C语言 三、实验原理 1. 信号量机制 信号量是一种特殊的变量,用于在多进程或多线程之间同步和互斥访问共享资源。信号量的值可以被多个进程或线程访问和修改,但只能通过特定的操作进行修改。一般来说,信号量可以用于以下两个目的: (1) 用于同步进程或线程,保证它们按照某种顺序执行。 (2) 用于互斥访问共享资源,保证同时只有一个进程或线程访问共享资源。 信号量的操作包括两种:P操作和V操作。P操作用于申请信号量,V操作用于释放信号量。具体来说,P操作会将信号量的值减1,如果减1后的值为负数,则表示资源已被占用,进程或线程需要等待;V操作会将信号量的值加1,如果加1后的值为非负数,则表示资源已释放,进程或线程可以继续执行。 2. 哲学家进餐问题 哲学家进餐问题是一种典型的并发编程问题,场景为五位哲学家围坐在一个圆桌旁,每位哲学家需要交替地进行思考和进餐,而进餐需要使用两个相邻的叉子,因此如果每位哲学家都拿起自己左边的叉子,就会导致死锁。为了避免死锁,需要采用一定的算法来保证哲学家们能够交替地进餐,而不会出现所有哲学家都在等待叉子的情况。 一种常用的解决方法是利用信号量机制,为每个叉子都创建一个信号量,表示该叉子是否可用。在每位哲学家进行进餐时,先判断其左右两个叉子是否都可用,如果都可用,则分别将左右两个叉子的信号量减1,表示占用了这两个叉子,然后开始进餐。进餐完成后,将左右两个叉子的信号量加1,表示释放了这两个叉子。 四、实验步骤 1. 定义信号量和哲学家的结构体: ``` #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #define N 5 // 哲学家数量 typedef struct { sem_t fork_left; // 左边的叉子 sem_t fork_right; // 右边的叉子 int id; // 哲学家编号 } philosopher_t; ``` 2. 创建哲学家和叉子的信号量: ``` int main() { philosopher_t philosophers[N]; sem_t forks[N]; for (int i = 0; i < N; i++) { sem_init(&forks[i], 0, 1); // 初始化叉子的信号量为1,表示可用 } for (int i = 0; i < N; i++) { philosophers[i].id = i; sem_init(&philosophers[i].fork_left, 0, 1); // 初始化左边叉子的信号量为1,表示可用 sem_init(&philosophers[i].fork_right, 0, 1); // 初始化右边叉子的信号量为1,表示可用 } // ... } ``` 3. 创建哲学家线程和进餐函数: ``` void *eat(void *arg) { philosopher_t *p = (philosopher_t *) arg; int id = p->id; sem_t *fork_left = &p->fork_left; sem_t *fork_right = &p->fork_right; while (1) { printf("Philosopher %d is thinking\n", id); sleep(1); printf("Philosopher %d is hungry\n", id); sem_wait(fork_left); // 申请左边的叉子 printf("Philosopher %d picks up fork %d\n", id, id); sem_wait(fork_right); // 申请右边的叉子 printf("Philosopher %d picks up fork %d\n", id, (id + 1) % N); printf("Philosopher %d is eating\n", id); sleep(1); sem_post(fork_left); // 释放左边的叉子 sem_post(fork_right); // 释放右边的叉子 printf("Philosopher %d puts down fork %d and fork %d\n", id, id, (id + 1) % N); } } int main() { philosopher_t philosophers[N]; sem_t forks[N]; // ... for (int i = 0; i < N; i++) { philosophers[i].id = i; sem_init(&philosophers[i].fork_left, 0, 1); sem_init(&philosophers[i].fork_right, 0, 1); pthread_t tid; pthread_create(&tid, NULL, eat, &philosophers[i]); // 创建哲学家线程 } pthread_exit(NULL); } ``` 五、实验结果 运行程序后,可以看到五位哲学家交替地进行思考和进餐,没有出现死锁的情况。 ``` Philosopher 0 is thinking Philosopher 1 is thinking Philosopher 2 is thinking Philosopher 3 is thinking Philosopher 4 is thinking Philosopher 0 is hungry Philosopher 0 picks up fork 0 Philosopher 0 picks up fork 1 Philosopher 0 is eating Philosopher 1 is hungry Philosopher 1 picks up fork 1 Philosopher 1 picks up fork 2 Philosopher 1 is eating Philosopher 2 is hungry Philosopher 2 picks up fork 2 Philosopher 2 picks up fork 3 Philosopher 2 is eating Philosopher 3 is hungry Philosopher 3 picks up fork 3 Philosopher 3 picks up fork 4 Philosopher 3 is eating Philosopher 4 is hungry Philosopher 4 picks up fork 4 Philosopher 4 picks up fork 0 Philosopher 4 is eating Philosopher 0 puts down fork 0 and fork 1 Philosopher 0 is thinking Philosopher 1 puts down fork 1 and fork 2 Philosopher 1 is thinking Philosopher 2 puts down fork 2 and fork 3 Philosopher 2 is thinking Philosopher 3 puts down fork 3 and fork 4 Philosopher 3 is thinking Philosopher 4 puts down fork 4 and fork 0 Philosopher 4 is thinking Philosopher 0 is hungry Philosopher 0 picks up fork 0 Philosopher 0 picks up fork 1 Philosopher 0 is eating Philosopher 1 is hungry Philosopher 1 picks up fork 1 Philosopher 1 picks up fork 2 Philosopher 1 is eating Philosopher 2 is hungry Philosopher 2 picks up fork 2 Philosopher 2 picks up fork 3 Philosopher 2 is eating Philosopher 3 is hungry Philosopher 3 picks up fork 3 Philosopher 3 picks up fork 4 Philosopher 3 is eating Philosopher 4 is hungry Philosopher 4 picks up fork 4 Philosopher 4 picks up fork 0 Philosopher 4 is eating Philosopher 0 puts down fork 0 and fork 1 Philosopher 0 is thinking Philosopher 1 puts down fork 1 and fork 2 Philosopher 1 is thinking Philosopher 2 puts down fork 2 and fork 3 Philosopher 2 is thinking Philosopher 3 puts down fork 3 and fork 4 Philosopher 3 is thinking Philosopher 4 puts down fork 4 and fork 0 Philosopher 4 is thinking ``` 六、实验结论 本实验利用信号量机制解决了哲学家进餐问题,保证了每位哲学家交替地进行思考和进餐,避免了死锁的情况。通过实验可以看出,信号量机制是一种非常有效的并发编程解决方法,具有很高的实用性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值