文章目录
- 1 生产者消费者问题
- 1.1 普通版本
- 1.2 复杂版本
- 2 读者-写者问题
- 3 哲学家进餐问题
- 4 吸烟者问题
- 5 理发店问题
- 5.1 普通版本
- 6 总结
1 生产者消费者问题
1.1 普通版本
- 问题描述
一组生产者进程和一组消费者进程共享一个初始为空、固定大小为n的缓存(缓冲区)。生产者的工作是制造一段数据,只有缓冲区没满时,生产者才能把消息放入到缓冲区,否则必须等待,如此反复;同时,只有缓冲区不空时,消费者才能从中取出消息,一次消费一段数据(即将其从缓存中移出),否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或者一个消费者从中取出消息。
- 问题分析
- 关系分析:生产者和消费者对缓冲区互斥访问是互斥关系,同时生产者和消费者又是一个相互协作的关系,只有生产者生产之后,消费者才能消费,它们也是同步关系。
- 整理思路:只有生产者和消费两个进程,正好是这两个进程存在着互斥关系和同步关系,我们需要解决的就是互斥和同步PV操作的位置。
- 信号量设置:信号量mutex作为互斥信号量,用于控制互斥访问缓冲池,互斥信号量初值为1;信号量full用于记录当前缓冲池中“满”缓冲区数,初值为0;信号量empty用于记录当前缓冲池中“空”缓冲区数,初值为n。
- 代码实现
semaphore mutex = 0; // 临界区互斥信号量
semaphore full = 0; // 缓冲区初始为空
semaphore empty = n; // 空闲缓冲区
producer() {
while (true) {
生产数据;
wait(empty); // 请求空闲缓冲区单元
wait(mutex); // 请求进入临界区
将数据放入缓冲区;
signal(mutex); // 离开临界区,释放互斥信号量
signal(full); // 已经生产了数据,满缓冲区数+1
}
}
consumer() {
while (true) {
wait(full); // 请求满缓冲区单元
wait(mutex); // 请求进入临界区
将数据从缓冲区中取出;
signal(mutex); // 离开临界区,释放互斥信号量
signal(empty); // 已经消费了数据,空缓冲区数+1
}
}
其中,对empty和full信号量的P操作必须放在对mutex信号量的P操作之前。如果生产者进程先执行wait(mutex)
,然后再执行signal(empty)
,假设生产者进程已经将缓冲区放满,此时mutex信号量被封锁,同时生产者进程执行signal(empty)
时被阻塞,此时消费者拿不到进入临界区的权限,消费者进程也会阻塞。这样造成死锁。同理消费者进程将缓冲区取空也会产生类似的死锁。
1.2 复杂版本
- 问题描述
桌子上有一个盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等吃盘子中的橘子,女儿专等吃盘子中的苹果。只有盘子为空时,爸爸或妈妈才可向盘子中放一个水果;仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出。
- 问题分析
- 关系分析:由于每次只能向其中放入一个水果可知,爸爸和妈妈时互斥关系。爸爸和女儿、妈妈和儿子是同步关系,而且这两对进程必须连起来,儿子和女儿之间没有互斥和同步关系,因为他们是选择条件执行,不可能并发。
- 整理思路:这里有四个进程,实际上可以抽象为两个生产者和两个消费者被连接到大小为1的缓冲区上。下图为进程之间的关系。
- 信号量设置:首先将plate设置为互斥信号量,用于控制互斥放入水果,初值为1。信号量apple表示盘子是否有苹果,初值为0。信号量orange表示盘子中是否有橘子,初值为0。
- 代码实现
semaphore plate = 1;
semaphore apple = 0;
semaphore orange = 0;
dad() {
while (true) {
放苹果前的一系列工作;
wait(plate); // 请求进入临界区,放苹果
向盘中放苹果;
signal(apple); // 已放入苹果,说明可以取苹果了
}
}
mom() {
while (true) {
放橘子前的一系列工作;
wait(plate); // 请求进入临界区,放橘子
向盘中放橘子;
signal(orange); // 已放入橘子,说明可以取橘子了
}
}
daughter() {
while (true) {
wait(apple); // 请求取苹果
拿苹果;
signal(plate); // 已拿完苹果,盘子为空,释放临界区,表示可以放东西了。
吃苹果;
}
}
son() {
while (true) {
wait(orange); // 请求拿橘子
拿橘子;
signal(orange); // 已拿完橘子,盘子为空,释放临界区,表示可以放东西了。
吃橘子。
}
}
2 读者-写者问题
- 问题描述
有读者和写者两组并发进程,共享一个文件,当两个或以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:①允许多个读者可以同时对文件执行读操作;②只允许一个写者往文件中写信息;③任一写者在完成写操作之前不允许其他读者或写者工作;④写者执行写操作前,应让已有的读者和写者全部退出。
- 问题分析
- 关系分析:由题可知,读者和写者是互斥的,写者和写者是互斥的,而读者和读者不存在互斥问题。
- 整理思路:两个进程,即读者和写者。写者是比较简单的,因为它和任何进程互斥,用互斥信号量的P操作、V操作即可解决。而读者的问题比较复杂,它必须在实现与写者互斥的同时,实现与其他读者的同步,因此简单的一对P操作、V操作是无法解决问题的。我们这里可以用一个计数器,用来判断当前是否有读者读文件。考虑两种方案:一种是有读者时,写者无法写文件,此时读者会一直占用文件;另一种是有读者时,写者先禁止后续读进程的请求,等到已在共享文件的读进程执行完毕,立即让写进程执行,只有在无写进程执行的情况才允许读进程再次运行。第一种方案称为读者优先,第二种方案称为写者优先。
- 读者优先方案
- 信号量设置:首先设置信号量count为计数器,用于记录当前读者的数量,初值为0;设置mutex为互斥信号量,用于保护更新count变量时的互斥,初值为1;设置互斥信号量rw,用于保证读者和写者的互斥访问,初值为1。
- 代码实现
int count = 0; // 用于记录当前的读者数量
semaphore mutex = 1; // 用于保护更新count变量时的互斥
semaphore rw = 1; // 用于保证读者和写者的互斥访问
wirter() {
while (true) {
wait(rw); // 请求进入临界区,写入共享文件
写入信息;
signal(rw); // 释放共享文件
}
}
reader() {
while (true) {
wait(count); // 互斥访问count变量,避免更新不一致
++ count; // 读者+1
if (count === 1) {
wait(rw); // 这里判断第一个读进程就锁住(避免重复锁住),阻止写进程写入。
}
signal(count); // 释放互斥变量count
读取共享文件;
wait(count); // 该读者访问完毕,互斥访问count变量,避免更新不一致
-- count; // 读者-1
if (count == 0) {
signal(rw); // 这里判断是否还有读进程,允许写进程写入
}
signal(count); // 释放互斥变量count
}
}
- 写者优先方案
- 信号量设置:相比读者优先方案,这里还需要一个信号量w,来实现“写者优先”,初值为1。
- 代码实现
int count = 0; // 用于记录当前的读者数量
semaphore mutex = 1; // 用于保护更新count变量时的互斥
semaphore rw = 1; // 用于保证读者和写者的互斥访问
semaphore w = 1; // 用于实现“写者优先”
writer() {
while (true) {
wait(w); // 在无写进程时进入
wait(rw); // 请求写入共享文件
写入信息;
signal(rw); // 释放共享文件
signal(w); // 恢复对共享文件的访问
}
}
reader() {
while (true) {
wait(w); // 在无写进程时进入
wait(count); // 互斥访问count变量,避免更新不一致
++ count; // 读者+1
if (count === 1) {
wait(rw); // 这里判断第一个读进程就锁住(避免重复锁住),阻止写进程写入。
}
signal(count); // 释放互斥变量count
读取共享文件;
signal(w); // 恢复对共享文件的访问
wait(count); // 该读者访问完毕,互斥访问count变量,避免更新不一致
-- count; // 读者-1
if (count == 0) {
signal(rw); // 这里判断是否还有读进程,允许写进程写入
}
signal(count); // 释放互斥变量count
}
}
这里的“写者优先”是相对而言的。当一个写进程访问文件时,先有一些读进程要求访问文件,后有另一个写进程要访问文件,则当前访问文件的进程结束对文件的写操作时,会是一个读进程而不是写进程占用文件(在信号量w的阻塞队列上,因为读进程先来,因此排在阻塞队列队首,而V操作唤醒进程时唤醒的是队首进程)。
3 哲学家进餐问题
- 问题描述
一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭,如下图所示。哲学家们倾注毕生精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿的时候,才试图拿起左、 右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿到了两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。
- 问题分析
- 关系分析:5名哲学家与左右邻居对其中间筷子的访问是互斥关系。
- 整理思路:显然这里有五个进程。本题的关键是如何让一个哲学家拿到左右两个筷子而不造成死锁或者饥饿现象。那么解决方法有两个,一个是让他们同时拿两个筷子;二是对每个哲学家的动作制定规则,避免饥饿或者死锁现象的发生。
- 信号量设置:定义互斥信号量数组chopstick[5] = {1, 1, 1, 1, 1},用于对5个筷子的互斥访问。哲学家按顺序编号0~4,哲学家i左边的筷子的编号为i,哲学家i右边的筷子的编号为(i+1)%5。
- 第一种方法:同时拿筷子
semaphore chopstick[5] = {1, 1, 1, 1, 1};
Pi() { // i号哲学家的进程
while (true) {
wait(chopstick[i]); // 取左边筷子
wait(chopstick[(i + 1) % 5]); // 取右边筷子
进餐;
signal(chopstick[i]); // 放回左边筷子
signal(chopstick[(i + 1) % 5]); // 取右边筷子
思考;
}
}
这种方案会产生死锁,即当5名哲学家都想要进餐并分别拿起左边的筷子时,那么所有的筷子都被拿光,然后拿右边筷子就会被阻塞。
- 第二种方法:制定规则
为了防止死锁发生,我们指定规则:当一名哲学家左右两边的筷子都可用时,才允许拿起筷子。这里我们需要添加一个互斥信号量mutex,避免同时取筷子,初值为1。
semaphore chopstick[5] = {1, 1, 1, 1, 1};
semaphore mutex; // 设置互斥取筷子的信号量。
Pi() { // i号哲学家的进程
while (true) {
wait(mutex); // 在取筷子之前获得信号量
wait(chopstick[i]); // 取左边筷子
wait(chopstick[(i + 1) % 5]); // 取右边筷子
signal(mutex); // 释放取筷子的信号量
进餐;
signal(chopstick[i]); // 放回左边筷子
signal(chopstick[(i + 1) % 5]); // 取右边筷子
思考;
}
}
4 吸烟者问题
- 问题描述
假设一个系统有三个抽烟者进程和一个供应者进程。每个抽烟者不停地卷烟并抽掉它,但是要卷起并抽掉一支烟,抽烟者需要有三种材料:烟草、纸和胶水。三个抽烟者中,第一个拥有烟草、第二个拥有纸,第三个拥有胶水。供应者进程无限地提供三种材料, 供应者每次将两种材料放到桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者一个信号告诉完成了,供应者就会放另外两种材料在桌上,这种过程一直重复(让三个抽烟者轮流地抽烟)。
- 问题分析
- 关系分析:供应者与三个抽烟者分别是同步关系。由于供应者无法同时满足两个或 以上的抽烟者,三个抽烟者对抽烟这个动作互斥。
- 整理思路:显然这里有四个进程,供应者作为生产者向3个抽烟者提供材料。
- 信号量设置:信号量offer1,offer2,offer3分别表示烟草和纸组合的资源、烟草和胶水组合的资源、纸和胶水组合的资源。信号量finish用于互斥进行抽烟动作。
- 代码实现
int num = 0; // 当前选中的材料,让抽烟者轮流抽烟
semaphore offer1 = 0, offer2 = 0, offer3 = 0;
semaphore finish = 0; // 互斥信号量
P0() { // 供应者进程
while (true) {
num = (num + 1) % 3; // 选择抽烟者所需的材料
if (num == 0) {
signal(offer1); // 提供烟草和纸
} else if (num == 1) {
signal(offer2); // 提供烟草和胶水
} else {
signal(offer3); // 提供纸和胶水
}
将材料放在桌上;
wait(finish); // 等待抽烟者抽完后再放材料
}
}
P1() { // 拥有胶水的抽烟者进程
while (true) {
wait(offer1); // 请求材料
拿材料,抽烟;
signal(finish); // 释放互斥信号量,表示抽烟者已抽完
}
}
P2() { // 拥有纸的抽烟者进程
while (true) {
wait(offer2); // 请求材料
拿材料,抽烟;
signal(finish); // 释放互斥信号量,表示抽烟者已抽完
}
}
P3() { // 拥有烟草的抽烟者进程
while (true) {
wait(offer3); // 请求材料
拿材料,抽烟;
signal(finish); // 释放互斥信号量,表示抽烟者已抽完
}
}
5 理发店问题
5.1 普通版本
- 问题描述
一个理发店由一个有n个椅子的等候室和一个有一个理发椅的理发室组成。其中:
- 如果有没有顾客来服务,理发师就去睡觉了。
- 如果顾客走进理发店和所有的椅子被占用了,然后顾客离开了商店。
- 如果理发师很忙,但是椅子是可用的,那么顾客坐在一张免费的椅子上。
- 如果理发师睡着了,顾客就会叫醒理发师。
- 问题分析
- 关系分析:由题可知,顾客之间对理发椅的访问是互斥关系。理发师和顾客又是一个相互协作的关系,只有理发师给顾客理发,顾客才能被理发,即是同步关系。
- 整理思路:这里有两个进程,和生产者消费者问题类似。
- 信号量设置:我们用整型变量count来表示空座椅,初值为n。首先顾客的座位被所有顾客进程共享,所以要设置互斥量mutex,来防止多个进程(线程)同时修改内存造成的错误,初值为1。理发师只有一个,所以需要设置信号量barber,初值为0(刚开始理发师在睡觉),来让顾客判断何时叫醒理发师。顾客可以最多有n个,需要设置信号量customer,表示顾客的数量,初值为0,让理发师判断是否去睡觉,顾客会在获得互斥锁后,查看剩余座位数,来决定走或者留。
- 代码实现
int count = n; // 所有椅子的数量
semaphore mutex = 1;
semaphore barber = 0;
semaphore customer = 0;
Barber() {
while (true) {
wait(customer); // 试图为一位顾客服务
wait(mutex); // 说明有顾客,这个时候就要被叫醒,此时空椅子数量+1
++ count;
signal(mutex); // 释放椅子信号量,使得进店的顾客决定是否进店等待。
signal(barber); // 现在有一个醒着的理发师,释放理发师信号量,让顾客竞争理发
理发;
}
}
Customer() {
while (true) {
wait(mutex); // 想做到椅子上去
if (count > 0) { // 说明还有空椅子
-- count;
signal(customer); // 通知理发师,顾客来了
signal(mutex); // 释放信号量
wait(barber); // 请求理发;
开始理发;
} else {
signal(mutex); // 没有空着的椅子,释放互斥信号量
}
}
}
6 总结
PV 操作题目的解题思路:
- 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。
- 整理思路。根据各进程的操作流程确定P、V操作的大致顺序。
- 设置信号量。设置需要的信号量,并根据题目条件确定信号量初值。(互斥信号量初值一般为1,同步信号量的初始值要看对应资源的初始值是多少)