操作系统
前言
就看408真题的考频,可以说,这一节有很大概率考大题。而互斥与同步本就是操作系统这门课相对而言的难点。本文参考22版王道操作系统,先回顾进程同步中的基本概念,后重点分析了几种经典同步问题,最后详细回答了王道的课后习题。本人尚在学习过程中,文章难免有纰漏,尤其是几处感觉王道书有问题的地方,也不敢确定自己的想法是否正确,希望大家辩证地看待。
进程同步
一、基本概念
1. 临界资源
多个进程可以共享系统中的各种资源,许多资源一次只能为一个进程所用,将一次仅允许一个进程所用的资源称为临界资源
物理设备:打印机
许多变量、数据等
临界资源的访问过程分成4个部分:
- 进入区:为了进入临界区使用临界资源,在进入区要检查可否进入临界区,若能进入临界区,则应设置正在访问临界区的标志,以阻止其他进程同时进入临界区
- 临界区:进程中访问临界资源的那段代码,又称临界段
- 退出区:将正在访问临界区的标志清除
- 剩余区:代码中的其余部分
2. 临界区
每个进程中访问临界资源的那段代码
3. 同步
这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。执行的前驱关系实质就是同步
4. 互斥
互斥也称间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待,当占用临界资源的进程退出临界区后,另一个进程才允许访问此临界资源
同步机制应遵循准则:
- 空闲等待
- 忙则等待
- 有限等待
- 让权等待
二、互斥的基本方法
软件实现算法
1.单标志法
turn
指示被允许进入临界区的进程编号
- P 0 P_0 P0 进程:
while (turn != 0);
critical section;
turn = 1;
remainder section;
- P 1 P_1 P1 进程:
while (turn != 1);
critical section;
turn = 0;
remainder section;
缺陷:必须交替进入临界区,若一个进程不再进入临界区,则另一个进程也无法进入临界区
- 违背空闲让进
举例: p 0 p0 p0 进入临界区并离开,此时临界区空闲,但 p 1 p1 p1 始终不进入临界区,此时 t u r n = 1 turn=1 turn=1 导致 p 0 p0 p0 无法进入临界区
2.双标志法先检查
先查看临界资源是否正在被访问,若正在被访问,则等待
否则,进入临界区
- P i P_i Pi 进程:
while (flag[j]); //(1)
flag[i] = True; //(3)
critical section;
flag[i] = False;
remainder section;
- P j P_j Pj 进程:
while (flag[i]); //(2)
flag[j] = True; //(4)
critical section;
flag[j] = False;
remainder section;
优点:
- 不用交替进入,可以连续使用
缺点:
- P i P_i Pi 和 P j P_j Pj 可能同时进入临界区,当按照 ( 1 ) ( 2 ) ( 3 ) ( 4 ) (1)(2)(3)(4) (1)(2)(3)(4) 的顺序执行时,二者标志位起初均为 F a l s e False False,导致都进入了临界区
- 违背忙则等待
- 这里的问题在于检查和修改操作不能一次性完成
3.双标志法后检查
可以看出,与双标志法前检查区别在于检查在修改的后面
- P i P_i Pi 进程:
flag[i] = True;
while (flag[j]);
critical section;
flag[i] = False;
remainder section;
- P j P_j Pj 进程:
flag[j] = True;
while (flag[i]);
critical section;
flag[j] = False;
remainder section;
缺点:
- 按照之前的思路,两个进程几乎同时要进入临界区,都将自己的标志位记为 T r u e True True,然后同时检测对方状态,发现对方也要进入临界区,导致互相谦让,结果谁都进不了
- 导致饥饿现象
4.Peterson’s Algorithm
先看代码:设置了 f l a g flag flag 标志位 和 t u r n turn turn 标志,可以发现是单标志法和双标志法后检查的结合
- P i P_i Pi 进程:
flag[i] = True;
turn = j;
while (flag[j] && turn == j);
critical section;
flag[i] = False;
remainder section;
- P j P_j Pj 进程:
flag[j] = True;
turn = i;
while (flag[i] && turn == i);
critical section;
flag[i] = False;
remainder section;
举个生活中的例子,两个人同时要进门,都特别谦让,在双标志法后检查中,谁都进不去。而本算法指定了一个 t u r n turn turn 标志,即主人指定谁先进来,这样其实就是二者先礼貌的客气一番,然后就按照主人的指定顺序入门了。很符合生活中的实际情况。
- 利用 f l a g flag flag 解决临界资源的互斥访问
- 利用 t u r n turn turn 解决饥饿问题
硬件实现算法
计算机提供了特殊的硬件指令,允许对一个字中的内容进行检测和修正,或对两个字的内容进行交换
通过硬件支持实现临界段问题的方法称为低级方法,或称为元方法
1.中断屏蔽方法
禁止一切中断发生,或称为屏蔽中断、关中断。保证不会发生进程切换,这样当前进程能顺利执行临界区代码,保证互斥
关中断
临界区
开中断
缺点:
- 限制了处理机交替执行程序的能力,效率低下
- 权力交给用户不太明智
2.硬件指令方法
T e s t A n d S e t TestAndSet TestAndSet 指令
原子操作,即执行该代码时不允许被中断。其功能是读出指定标志后把该标志设成真
boolean TestAndSet (boolean *lock)
{
boolean old;
old = *lock;
*lock = true;
return old;
}
为每个临界资源设置一个共享布尔变量 l o c k lock lock
表示资源两种状态:
- T r u e True True 表示被占用
- F a l s e False False 表示未被占用, l o c k lock lock 初值为 F a l s e False False
利用 T e s t A n d S e t TestAndSet TestAndSet 检查和修改标志 l o c k lock lock,若有进程在临界区,则重复检查,直到进程退出。
用该指令实现进程互斥算法如下:
while TestAndSet(&lock);
进程的临界区代码段;
lock=false;
进程的其他代码;
S w a p 指 令 Swap指令 Swap指令
功能:交换两个字(字节)的内容
功能描述如下:
Swap(boolean *a, boolean *b)
{
boolean temp;
temp = *a;
*a = *b;
*b = temp;
}
注意:以上指令描述仅仅是功能实现,而并非软件实现的定义,事实上,是由 硬件逻辑 直接实现的,不会被中断
应该为每个临界资源设置一个 共享 布尔类型变量 lock
,初值为 flase
;在 每个进程中再设置一个 局部 布尔变量 key
,用于与 lock
交换信息。在进入临界区前,先利用
S
w
a
p
Swap
Swap 指令交换 lock
与 key
的内容,然后检查 key
的状态;有进程在临界区时,重复交换和检查过程,直到进程退出。
key = true;
while(key !- false)
{
Swap(&lock, &key);
}
进程的临界区代码段;
lock=false;
进程的其他代码;
硬件方法的优点:适用于任意数目的进程,而不管是单处理机还是多处理机。可支持进程中有多个临界区,只需为每个临界区设立一个布尔变量
硬件方法的缺点:进程等待进入临界区要耗费处理机时间,不能实现 让权等待。从等待进程中随机选择一个进入临界区,有的进程可能选不上,导致 饥饿
三、信号量
信号量用来解决互斥和同步问题,它只能被两个标准的 原语 w a i t ( S ) wait(S) wait(S) 和 s i g n a l ( S ) signal(S) signal(S) 访问,也可以记为 P操作 和 V操作
原语 指完成某种功能且不被分割、不被中断执行的操作序列,通常可由硬件来实现。
1.整型信号量
整型信号量被定义为一个用于表示资源数目的整型量 S S S
wait(S)
{
while(S<=0);
S = S-1;
}
signal(S)
{
S=S+1;
}
信号量 S ≤ 0 S\le 0 S≤0,就会不断地测试,因此并未遵循 让权等待 的原则
2.记录型信号量
不存在 忙等 现象。除了一个用于代表资源数目的整型变量 v a l u e value value 外,再增加一个进程链表 L L L,用于链接所有等待该资源的进程。
记录型信号量描述:
typedef struct
{
int value;
struct process *L;
} semaphore;
相应的 w a i t ( S ) wait(S) wait(S) 和 s i g n a l ( S ) signal(S) signal(S) 操作如下:
void wait(semaphore S)
{
S.value--;
if (S.value < 0)
{
add this process to S.L;
block(S.L);
}
}
关键就是在发现资源已分配完后进行自我阻塞,放弃处理机,进入等待队列,遵循 让权等待 准则
void signal(semaphore S)
{
S.value++;
if (S.value <= 0)
{
remove a process P from S.L;
wakeup(P);
}
}
进程释放一个资源,如果还有等待进程,应该唤醒第一个。
3.利用信号量实现同步
设 S S S 为实现进程 P 1 , P 2 P_{1},P_{2} P1,P2 同步的公共信号量,初值为零。
semaphore S = 0;
P1()
{
...
x;
V(S);
...
}
P2()
{
...
P(S);
y;
...
}
同步是有先后顺序的,在这里 P 2 P_{2} P2 进程中的y语句要用到进程 P 1 P_{1} P1 中语句x的运行结果,即现有x才能有y
所以信号量初值设为零,如果 P 2 P_{2} P2 先执行,就会被阻塞,保证了正确的 先后顺序
4.利用信号量实现进程互斥
设 S S S 为实现进程 P 1 , P 2 P_{1},P_{2} P1,P2 互斥的信号量,初值为一。(每次只允许一个进程进入临界区,也就是资源数为一)。
semaphore S = 1;
P1()
{
...
P(S); //上锁
进程P1的临界区;
V(S); //解锁
...
}
P2()
{
...
P(S); //上锁
进程P2的临界区;
V(S); //解锁
...
}
5.利用信号量实现前驱关系
上图是一个前驱图,信号量初始值均设为零。为了保证 S 1 → S 2 , S 1 → S 3 S_{1}\to S_{2},S_{1}\to S_{3} S1→S2,S1→S3 的前驱关系,设置信号量 a 1 , a 2 a_{1},a_{2} a1,a2 ,以此类推
semaphore a1=a2=b1=b2=c=d=e=0;
S1()
{
...
V(a1);
V(a2);
}
S2()
{
P(a1);
...
V(b1);
V(b2);
}
S3()
{
P(a2);
...
V(c);
}
S4()
{
P(b1);
...
V(d);
}
S5()
{
P(b2);
...
V(e);
}
S6()
{
P(c);
P(d);
P(e);
...
}
因该很容易看出来,其实这就是多层的同步问题,并不复杂,主要是抓住一个要点:你依赖谁?谁依赖你?对于你依赖的进程,你要 P P P ,其实就是问问它搞好了没有,好了就把数据发你,你就可以开始工作了。如果你去问他,他没好,那么你就在那等,它好了再叫你。同理,谁依赖你,它也就在那等你,所以你数据处理后就 V V V 发给它。搞明白先后顺序,问题就迎刃而解了。
四、管程
1.管程的定义
信号量操作的缺点?
- 程序员写程序时必须特别注意顺序问题。编写程序困难,易出错
什么是管程?
- 利用共享数据结构抽象地表示系统中的共享资源,而把对该数据结构实施的操作定义为一组过程。进程对共享资源的申请,释放等操作,都由这组过程来实现。这组过程还可以根据资源情况,或接受或阻塞进程的访问,确保每次仅有一个进程使用共享资源,这样就能统一管理对共享资源的所有访问,实现进程互斥。
管程由4部分组成:
- 管程的名称
- 局部于管程内部的共享数据结构说明
- 对该数据结构进行操作的一组过程
- 对局部于管程内部的共享数据设置初始化的语句
2.条件变量
设置条件变量以及等待/唤醒操作以解决同步问题。
每个条件变量保存一个等待队列,用于记录因为该条件变量而阻塞的所有进程,对条件变量只能进行两种操作,即 w a i t wait wait 和 s i g n a l signal signal
条件变量和信号量的比较:
- 相似点:条件变量的 w a i t / s i g n a l wait/signal wait/signal 操作类似于信号量的 P / V P/V P/V 操作,可以实现进程的阻塞/唤醒
- 不同点:条件变量没有值,仅实现排队等待功能;而信号量有值,反映剩余资源数
3.用管程解决生产者消费者问题
王道伪代码如下:
右边是生产者消费者进程代码,可见管程封装程度之高。
五、经典同步问题
1.生产者-消费者问题
问题描述
一组生产者进程和一组消费者进程共享一个初始为空、大小为n的缓冲区,只有缓冲区没满时,生产者才能把消息放入缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中取出消息,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或一个消费者从中取出消息。
问题分析
- 生产者消费者互斥访问缓冲区
- 生产者消费者是相互协作关系(个人理解:前文所说的同步问题是单方向的,即先。。。再。。。,而这里的互相协作代表了同步是双向的,即有时要先等生产者生产后消费者才能消费,有时要等消费者消费后生产者才能生产。而这里的有时就由信号量决定。因为有两个同步关系,所以在解决同步问题时需要有两个信号量。)
- 信号量 m u t e x mutex mutex 作为互斥信号量,初值为一
- 信号量 f u l l full full 表示缓冲区有物品(满)的数量,初值为零,用来解决 生 产 者 → 消 费 者 生产者 \to 消费者 生产者→消费者 的同步问题。信号量为零时,必须生产者先生产商品,消费者才能消费。
- 信号量 e m p t y empty empty 表示缓冲区没有物品(空)的数量,初值为 n n n,用来解决 消 费 者 → 生 产 者 消费者 \to 生产者 消费者→生产者 的同步问题,信号量为零时,必须消费者先消费商品,生产者才能生产
代码展示
semaphore mutex = 1;
semaphore full = 0;
semaphore empty = n;
producer()
{
while (1)
{
produce an item in nextp;
P(empty);
P(mutex);
add nextp to buffer;
V(mutex);
V(full);
}
}
consumer()
{
while (1)
{
P(full);
P(mutex);
remove an item from buffer;
V(mutex);
V(empty);
consume the item;
}
}
若生产者进程先 P ( m u t e x ) P(mutex) P(mutex),然后执行 P ( e m p t y ) P(empty) P(empty),消费者先执行 P ( m u t e x ) P(mutex) P(mutex),然后执行 P ( f u l l ) P(full) P(full),可不可以?
- 不行,假设缓冲区已满,先运行生产者进程,它先互斥加锁,然后执行 P ( e m p t y ) P(empty) P(empty) 被阻塞,这是去运行消费者进程,执行 P ( m u t e x ) P(mutex) P(mutex),但此时锁在生产者进程,因此消费者进程也被阻塞,导致无休止的等待。
再看一个较为复杂的生产者消费者问题
问题描述
桌子上有一个盘子,每次只能向其中放入ー个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等吃盘子中的橘子,女儿专等吃盘子中的苹果。只有盘子为空时,爸爸或妈妈才可向盘子中放一个水果:仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出。
问题分析
- 信号量 p l a t e plate plate 作为同步信号量,初值为一,用来解决 盘 子 变 空 事 件 → 放 水 果 事 件 盘子变空事件 \to 放水果事件 盘子变空事件→放水果事件,也就是先盘子为空,才能放水果。但其实盘子为空事件可以由女儿或者儿子引起,所以进程同步不能从单一进程角度考虑,而是由系列事件的先后顺序角度出发,加以分析!
- 信号量 a p p l e apple apple 表示缓冲区苹果的数量,初值为零,用来解决 爸 爸 → 女 儿 爸爸 \to 女儿 爸爸→女儿 的同步问题,信号量为零时,必须爸爸先放苹果,女儿才能消费
- 信号量 o r a n g e orange orange 表示缓冲区橘子的数量,初值为零,用来解决 妈 妈 → 儿 子 妈妈 \to 儿子 妈妈→儿子 的同步问题,信号量为零时,必须妈妈先放橘子,儿子才能消费
🚫 王道书里plate是互斥信号量,但其实完全不是这样的!按照我之前的推论和在王道代码中的实际位置和效果,也可以看出它实际上是一个同步信号量。**先盘子为空,才能放水果。**王道书中我觉得有点误人子弟了。
后来看了咸鱼学长的视频,发现他讲的是正确的,也介绍了为什么可以不写互斥。可能写书的不是他吧。还是建议2倍速把咸鱼视频过一遍再看书,效率奇高。
上图是加了互斥信号量的情况。那么为什么可以去掉?
原因在于本题缓冲区大小为1,在任何时刻,三个同步信号量最多只有一个值为一。因此在任何时刻,最多只有一个进程的 P P P 操作不会被阻塞,并顺利进入临界区。
问:那缓冲区大小为1,一定不用设置互斥信号量?
答:只是有可能不需要设置,还是要具体问题具体分析。
代码展示
semaphore plate = 1;
semaphore apple = 0;
semaphore orange = 0;
dad()
{
while (1)
{
prepare an apple;
P(plate);
put the apple on the plate;
V(apple);
}
}
mom()
{
while (1)
{
prepare an orange;
P(plate);
put the orange on the plate;
V(orange);
}
}
son()
{
while (1)
{
P(orange);
take an orange from the plate;
V(plate);
eat the orange;
}
}
daughter()
{
while (1)
{
P(apple);
take an apple from the plate;
V(plate);
eat the apple;
}
}
这里的互斥锁夹的实际上是两个进程,因为爸爸和女儿,妈妈和儿子必须连续的执行。所以个人理解(不一定对)为爸爸和女儿,妈妈和儿子这两个流程互斥
2.读者-写者问题
问题描述
有读者和写者两组并发进程,共享一个文件,当两个或以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:①允许多个读者可以同时对文件执行读操作;②只允许一个写者往文件中写信息;③任一写者在完成写操作之前不允许其他读者或写者工作;④写者执行写操作前,应让已有的读者和写者全部退出。
问题分析
题意:任一写者在完成写操作之前不允许其他读者或写者工作
- 写者要与读者互斥,但同时也要与其他写者互斥
题意:允许多个读者可以同时对文件执行读操作
- 这说明仅有一个互斥信号量不能解决问题,因为一个互斥信号量只能让每次只有一个进程访问临界资源。这导致多个读者之间也互斥,不符合题意。造成这的原因是每次读者进程都会进行一次 P P P 操作,那么为了使多个读者可以同时对文件执行读操作,我只有在第一个读者进程访问时上锁,在最后一个读者进程离开时解锁。所以需要一个计数器判断当前在读文件的读者数量。
信号量设置
- 信号量 r w rw rw 为互斥信号量,初值为一,保证读者和写者的互斥访问
- 信号量 m u t e x mutex mutex 为互斥信号量,初值为一,保证计数器在更新时的互斥访问
代码展示
int count = 0;
semaphore rw = 1;
semaphore mutex = 1;
writer()
{
while (1)
{
P(rw);
writing;
V(rw);
}
}
reader()
{
while (1)
{
P(mutex);
if (count == 0)
{
P(rw);
}
count++;
V(mutex);
reading;
P(mutex);
count--;
if (count == 0)
{
V(rw);
}
V(mutex);
}
}
上面算法种,读者进程是优先的,也就是说只要有一个读者进程活跃,随后而来的读者进程都可以访问文件,而写者进程却被长时间阻塞,存在长时间饿死的情况。
代码优化
如果希望写优先,当读进程正在读文件时,有一个写进程请求,就应该阻止后续的读进程请求,等到当前访问文件的读进程们执行完毕,立刻让等待的写进程执行。在无写进程执行的情况下,才放先前排队的读进程们执行。
- 增加一个互斥信号量 w w w,初值为一,用于保证“写优先”
int count = 0;
semaphore rw = 1;
semaphore mutex = 1;
semaphore w = 1;
writer()
{
while (1)
{
P(w);
P(rw);
writing;
V(rw);
V(w);
}
}
reader()
{
while (1)
{
P(w);
P(mutex);
if (count == 0)
{
P(rw);
}
count++;
V(mutex);
V(w);
reading;
P(mutex);
count--;
if (count == 0)
{
V(rw);
}
V(mutex);
}
}
注意 w w w 信号量的位置。为什么怎么加?我们可以推一下看看
- 问: 读 者 1 → 写 者 1 → 读 者 2 读者1\to写者1\to读者2 读者1→写者1→读者2
- 答:读者1正在读文件时,写者1来了,会被 r w rw rw 信号量阻塞,读者2来了,会被 w w w 信号量阻塞。在读者1读完撤离后,$ V(rw)$ 保证了写者1紧随其后运行,写者1撤离后, V ( w ) V(w) V(w) 保证读者2紧随其后运行
- 问: 写 者 1 → 读 者 1 → 写 者 2 写者1\to读者1\to写者2 写者1→读者1→写者2
- 答:写者1正在写文件,读者1来了,会被 w w w 信号量阻塞,读者2来了,也会被 w w w 信号量阻塞。信号量的阻塞队列保证了先来先服务,所以读者1会先执行。这就导致并不是“写进程优先”,实际上是读写公平
注意:写进程优先是相对而言,实际上可以叫做读写公平法。
3.哲学家进餐问题
问题描述
一张圆桌边上坐着5名哲学家,每两名哲学家之间的桌上摆一根筷子,两根筷子中间是一碗米饭。哲学家们倾注毕生精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根一根地拿起)。若筷子已在他人手上,则需要等待。饥饿的哲学家只有同时拿到了两根筷子才可以开始进餐,进餐完毕后,放下筷子继续思考。
问题分析
每名哲学家与左右邻居对在其中间的筷子是互斥访问
问题的关键就在于如何拿到左右筷子而不造成死锁
方法一:同时拿左右两根筷子
方法二:对每个哲学家的动作制定规则,避免死锁现象的发生
信号量设置:
- 信号量数组 c h o p s t i c k [ 5 ] = { 1 , 1 , 1 , 1 , 1 } chopstick[5]= \{1,1,1,1,1\} chopstick[5]={1,1,1,1,1},用于对五个筷子互斥访问。哲学家按顺序编号为 0 ∼ 4 0\sim4 0∼4,哲学家 i i i 左边筷子的编号为 i i i,哲学家右边筷子的编号为 ( i + 1 ) % 5 (i+1)\%5 (i+1)%5
代码展示
有问题的代码
semaphore chopstick[5] = {1, 1, 1, 1, 1};
Pi()
{
do
{
P(chopstick[i]);
P(chopstick[(i + 1) % 5]);
eat;
V(chopstick[i]);
V(chopstick[(i + 1) % 5]);
think;
} while (1);
}
存在问题:5名哲学家同时想吃饭并都拿起左边的筷子。筷子都被拿完,这时他们想拿右边的筷子,就全被阻塞,产生死锁。
问:如何防止死锁的发生?
答:施加一些限制条件
方法一:最多允许四位哲学家同时进餐,这样可以保证至少有一个哲学家是可以拿到左右筷子的
方法二:要求奇数号哲学家先拿左边的筷子,偶数号哲学家先拿右边的筷子。这样可以保证相邻的奇偶号哲学家都想吃饭时,只会有其中一个可以拿起第一只筷子,另一个直接阻塞。这样避免了占有一只后再等待另一只的情况。
方法三:仅当一名哲学家左右两边筷子都可用时,才允许哲学家拿起筷子(王道书上原话,但其实并不准确)。准确说法为:各个哲学家拿筷子这件事是互斥的,这样就保证一个哲学家拿筷子拿到一半时被阻塞,也不会有别的哲学家继续尝试拿筷子。这样的话,当正在吃饭的哲学家放下筷子后,被阻塞的哲学家就可以获得等待的筷子了。
采用方法三,代码如下:
semaphore chopstick[5] = {1, 1, 1, 1, 1};
semaphore mutex = 1;
Pi()
{
do
{
P(mutex);
P(chopstick[i]);
P(chopstick[(i + 1) % 5]);
V(mutex);
eat;
V(chopstick[i]);
V(chopstick[(i + 1) % 5]);
think;
} while (1);
}
大部分练习题和真题用消费者-生产者模型或读者-写者问题就能解决,但对于哲学家进餐问题和吸烟者问题仍然要熟悉。考研复习的关键在于反复多次和全面,“偷工减料”是要吃亏的。
4.吸烟者问题
问题描述
假设一个系统有三个抽烟者进程和一个供应者进程。每个抽烟者不停地卷烟并抽掉它,但要卷起并抽掉一支烟,抽烟者需要有三种材料:烟草、纸和胶水。三个抽烟者中,第一个拥有烟草,第二个拥有纸,第三个拥有胶水。供应者进程无限地提供三种材料,供应者每次将两种材料放到桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者一个信号告诉已完成,此时供应者就会将另外两种材料放到桌上,如此重复(让三个抽烟者轮流地抽烟)。
问题分析
信号量设置
- 信号量 o f f e r 1 offer1 offer1 表示烟草和纸组合的资源数,初值为0,用来描述 供 应 者 提 供 烟 草 和 纸 组 合 → 一 号 吸 烟 者 卷 烟 供应者提供烟草和纸组合 \to 一号吸烟者卷烟 供应者提供烟草和纸组合→一号吸烟者卷烟 的同步问题
- 信号量 o f f e r 2 offer2 offer2 表示烟草和胶水组合的资源数,初值为0,用来描述 供 应 者 提 供 烟 草 和 胶 水 组 合 → 二 号 吸 烟 者 卷 烟 供应者提供烟草和胶水组合 \to 二号吸烟者卷烟 供应者提供烟草和胶水组合→二号吸烟者卷烟 的同步问题
- 信号量 o f f e r 3 offer3 offer3 表示纸和胶水组合的资源数,初值为0,用来描述 供 应 者 提 供 纸 和 胶 水 组 合 → 三 号 吸 烟 者 卷 烟 供应者提供纸和胶水组合 \to 三号吸烟者卷烟 供应者提供纸和胶水组合→三号吸烟者卷烟 的同步问题
- 信号量 f i n i s h finish finish 作为同步信号量,初值为0,用来描述 抽 掉 烟 事 件 → 放 材 料 事 件 抽掉烟事件 \to 放材料事件 抽掉烟事件→放材料事件,我这里说事件,就是为了强调引起抽掉烟这一事件的主体不是一个进程,而是吸烟者1,2,3都有可能引发这一事件。
代码展示
int num = 0;
semaphore offer1 = 0;
semaphore offer2 = 0;
semaphore offer3 = 0;
semaphore finish = 0;
process P1()
{
while (1)
{
num++;
num = num % 3;
任意两种材料放在桌上;
if (num == 0)
{
V(offer1);
}
else if (num == 1)
{
V(offer2);
}
else
{
V(offer3);
}
P(finish);
}
}
process P2()
{
while (1)
{
P(offer1);
卷烟抽;
V(finish);
}
}
process P3()
{
while (1)
{
P(offer2);
卷烟抽;
V(finish);
}
}
process P4()
{
while (1)
{
P(offer3);
卷烟抽;
V(finish);
}
}
下图是王道书中供应者的相关代码
我觉得是有问题的。我的改动地方就是“任意两种材料放在桌子上”这句话的位置。按照王道的逻辑,我 V V V 完后被切到对应的那个吸烟者进程,那个进程就可以进行卷烟吸烟的操作。但按照逻辑来说,你材料实际上并没有放到缓冲区,这是于事实相矛盾的!
还有要强调的是这里供应者的 P V PV PV 顺序可不可以换。我认为是可以的,当然前提是改 f i n i s h finish finish 初值为1。
六、王道习题详解
大题1
题目
答案
当共享资源用共享数据结构表示时,资源管理程序可用对该数据结构进行操作的一组过程来表示,如资源的请求和释放过程request和release。。把这样一组相关的数据结构和过程一并归为管程。Hansan为管程所下的定义是:“一个管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据。”由定义可知,管程由三部分组成:
1)局部于管程的共享变量说明。
2)该数据结构进行操作的一组过程。
3)对局部于管程的数据设置初始值的语句;此外,还需为该管程赋予一个名字。
管程的引入是为了解决临界区分散所带来的管理和控制问题。在没有管程之前,对临界区的访问分散在各个进程之中,不易发现和纠正分散在用户程序中的不正确使用,V操作等问题。管程将这些分散在各进程中的临界区集中起来,并加以控制和管理,管程一次只允许一个进程进入管程内,从而既便于系统管理共享资源,又能保证互斥。
大题2
题目
答案
1)是互斥关系,同一本书只能被一名学生借阅,或任何时刻只能有一名学生借阅一本书。
2)是互斥关系,篮球是互斥资源,只可被一个队伍获得。
3)是同步关系,一个工序完成后开始下一个工序。
4)是同步关系,生产商品后才能消费。
大题3
题目
答案
信号量设置
- 互斥信号量 m u t e x mutex mutex,初值为1
- 同步信号量 o d d odd odd,初值为0,表示奇数的个数。生产者先放一个奇数, P 2 P_2 P2 才能取
- 同步信号量 e v e n even even,初值为0,表示偶数的个数。生产者先放一个偶数, P 3 P_3 P3 才能取
- 同步信号量 e m p t y empty empty,初值为 n n n,表示缓冲区没有物品(空)的数量。先消费者取出东西,生产者才能再放东西
semaphore mutex = 1;
semaphore odd = 0;
semaphore even = 0;
semaphore empty = n;
P1()
{
while (1)
{
x = produce();
P(empty);
P(mutex);
put(x);
V(mutex);
if (x % 2)
{
V(odd);
}
else
{
V(even);
}
}
}
P2()
{
while (1)
{
P(odd);
P(mutex);
getodd();
V(mutex);
V(empty);
countodd();
}
}
P3()
{
while (1)
{
P(even);
P(mutex);
geteven();
V(mutex);
V(empty);
counteven();
}
}
大题4
题目
答案
不能, x x x 作为临界资源没有被互斥访问
直接上答案
大题5
题目
答案
这题还是挺坑的,没考虑全,因为甚至在if语句里x都有切换进程,x值改变的可能性!
z的值: − 2 , 1 , 2 , 3 , 5 , 7 -2,1,2,3,5,7 −2,1,2,3,5,7
c的值: 9 , 25 , 81 9,25,81 9,25,81
大题6
题目
答案
对于条件二显然需要同步信号量。这个要求如何转化?假设信号量 S a S_a Sa,实际上就是要把这个不等式的临界值转化成信号量 S a S_a Sa 为零时的含义。题目中,A产品和B产品数量差为 M M M 时,我的信号量 S a S_a Sa 应该值为零。当我的 S a S_a Sa 表示什么的时候,满足条件?自然而然推出 S a S_a Sa 表示A产品还可以放的产品数量(按理来说A和B的数量差应该由A和B一起控制,这里我只用A表示的原因是只有在A产品生产时才有可能会到达临界情况)。
信号量设置
- 互斥信号量 m u t e x mutex mutex,初值为1
- 同步信号量 S a S_a Sa,初值为 M − 1 M-1 M−1 ,表示产品A还可以放的数量
- 同步信号量 S b S_b Sb,初值为 N − 1 N-1 N−1 ,表示产品B还可以放的数量
semaphore mutex = 1;
semaphore Sa = M - 1;
semaphore Sb = N - 1;
process_A()
{
while (1)
{
P(Sa);
P(mutex);
A产品入库;
V(mutex);
V(Sb);
}
}
process_B()
{
while (1)
{
P(Sb);
P(mutex);
B产品入库;
V(mutex);
V(Sa);
}
}
大题7
题目
答案
信号量设置
- 同步信号量 t a k e n u m takenum takenum,初值为 0 0 0,表示已经取号的顾客数量
- 同步信号量 c a l l n u m callnum callnum,初值为 0 0 0,表示空闲可以叫号的销售员数量
semaphore takenum = 0;
semaphore callnum = 0;
salesman()
{
while (1)
{
P(takemun);
叫号;
推销面包;
V(callnum);
}
}
consumer()
{
进店;
取号;
V(takenum);
P(callnum);
被服务;
}
大题8
题目
答案
信号量设置
- 同步信号量 e m p t y empty empty,初值为 500 500 500,表示还可以容纳多少人
- 互斥信号量 m u t e x mutex mutex,初值为1,可使出入口一次仅一人通过
semaphore empty = 500;
semaphore mutex = 1;
cobegin
参观者进程i:
{
...
P(empty);
P(mutex);
进门;
V(mutex);
参观;
P(mutex);
出门;
V(mutex);
V(empty);
...
}
coend
大题9
题目
答案
信号量设置
- 互斥信号量 m u t e x 1 mutex1 mutex1,初值为一,表示互斥访问 F 1 F_1 F1
- 互斥信号量 m u t e x 2 mutex2 mutex2,初值为一,表示互斥访问 F 2 F_2 F2
- 同步信号量 e m p t y 1 empty1 empty1,初值为10,表示 F 1 F_1 F1 空闲的容量
- 同步信号量 e m p t y 2 empty2 empty2,初值为10,表示 F 2 F_2 F2 空闲的容量
- 同步信号量 f u l l 1 full1 full1,初值为0,表示 F 1 F_1 F1 已用的容量
- 同步信号量 f u l l 2 full2 full2,初值为0,表示 F 2 F_2 F2 已用的容量
semaphore mutex1 = 1;
semaphore mutex2 = 1;
semaphore empty1 = 10;
semaphore empty2 = 10;
semaphore full1 = 0;
semaphore full2 = 0;
producer_A()
{
while (1)
{
生产零件A;
P(empty1);
P(mutex1);
放到货架F1;
V(mutex1);
V(full1);
}
}
producer_B()
{
while (1)
{
生产零件B;
P(empty2);
P(mutex2);
放到货架F2;
V(mutex2);
V(full2);
}
}
consumer()
{
while (1)
{
P(full1);
P(mutex1);
取零件A;
V(mutex1);
V(empty1);
P(full2);
P(mutex2);
取零件B;
V(mutex2);
V(empty2);
组装产品;
}
}
大题10
题目
答案
信号量设置
- 互斥信号量 v a t vat vat,互斥访问水缸
- 互斥信号量 w e l l well well,互斥访问水井
- 互斥信号量 p a i l pail pail,初值为3,互斥的访问水桶
- 同步信号量 e m p t y empty empty,初值为10,表示缸剩余可以容纳的水的桶数
- 同步信号量 f u l l full full,初值为0,表示缸中已有的水的桶数
semaphore vat = 1;
semaphore well = 1;
semaphore pail = 3;
semaphore empty = 10;
semaphore full = 0;
old()
{
while (1)
{
P(full);
P(pail);
P(vat);
打水;
V(vat);
V(empty);
喝水;
V(pail);
}
}
young()
{
while (1)
{
P(empty);
P(pail);
P(well);
从井里打水;
V(well);
P(vat);
倒水;
V(vat);
V(full);
V(pail);
}
}
易错点
- 要先看水缸容量后再决定是否拿起水桶,如果顺序颠倒,会引发死锁。
大题11
题目
答案
理一下关系,数据abc依次输入,所以要保证先p1再p2最后p3的顺序。
- 同步信号量 S 2 S2 S2,初值为零,用来维护先p1后p2的顺序
- 同步信号量 S 3 S3 S3,初值为零,用来维护先p2后p3的顺序
- 王道答案的 S 1 S1 S1 其实是没有必要的,因为3个进程中后2个进程都会被阻塞,只有P1可以运行。互斥信号量也不需要,因为在同一时刻只会有一个设备正常运行未被阻塞。
p1需要p2中的数据b
- 同步信号量 S b Sb Sb,初值为零,用来维护先b被p2取出后 x = a + b x=a+b x=a+b 才能执行这一顺序
p1需要等y和z都计算出后才能通过打印机打印
- 同步信号量 S y Sy Sy,初值为零,用来维护先y计算出后p1通过打印机打印的先后顺序
- 同步信号量 S z Sz Sz,初值为零,用来维护先z计算出后p1通过打印机打印的先后顺序
semaphore S2 = 0;
semaphore S3 = 0;
semaphore Sb = 0;
semaphore Sy = 0;
semaphore Sz = 0;
p1()
{
读取数据a;
V(S2);
P(Sb);
x = a + b;
P(Sy);
P(Sz);
打印机打印;
}
p2()
{
P(S2);
读取数据b;
V(Sb);
y = a * b;
V(S3);
V(Sy);
}
p3()
{
P(S3);
读取数据c;
z = y + c - a;
V(Sz);
}
发现写出来和王道答案不一样,我又推了一下,感觉自己的也没问题
我是在 y = a ∗ b y=a*b y=a∗b 这一计算结束后才允许p3唤醒,这就保证了p3运行时y的值已有。而王道是先允许p3允许读取数据c,后等待 S y Sy Sy 同步信号量。区别就在是否可以先读取数据c上,但题目中并未对数据c不是临界资源,所以何使读取无所谓。然后衍生一下,发现其实 S y Sy Sy 可以不需要,王道是让p1等待 S y Sy Sy 和 S z Sz Sz 两个信号量,但按照我的逻辑,已经保证了y和z的先后顺序,所以不需要 S y Sy Sy。当然王道的逻辑肯定更加严谨,我也不保证我的理解对,纯粹突发奇想。
大题12
题目
答案
信号量设置
- 同步信号量 e m p t y empty empty,初值为 10 10 10,表示空座位的数量,先有空座位,顾客才能取号
- 互斥信号量 m u t e x mutex mutex,初值为 1 1 1,互斥使用取号机
- 同步信号量 f u l l full full,初值为 1 1 1,表示座位上有顾客的数量(已占座位人数)。先座位上有顾客,营业员才能叫号
- 同步信号量 s e r v i c e service service,初值为0,顾客获得空座位后,要等待叫号后才能被服务
semaphore empty = 10;
semaphore mutex = 1;
semaphore full = 0;
semaphore service = 0;
cobegin
{
process 顾客i
{
P(empty);
P(mutex);
从取号机获取一个号码;
V(mutex);
V(full);
P(service);
接受服务;
}
Process 营业员
{
while (True)
{
P(full);
V(empty);
V(service);
为顾客服务;
}
}
}
coend
这是王道的代码,我下面说一下自己的理解。
我解决同步问题一直的思路就是看临界状态,即什么时候被阻塞,可以写成在…的数量为零时被阻塞就算解决。但在解释 s e r v i c e service service 时却很难说通。等待营业员叫号的人数?营业员叫号的人数?这些都说不通。最后找了一个较为合理的解释:营业员可以服务的人数。那么在 V ( s e r v i c e ) V(service) V(service) 后,值为一,表示营业员可以服务的人数为一,这时一位顾客 P ( s e r v i c e ) P(service) P(service) 就可以正常接受服务,而剩下的顾客实际上就是在这个信号量下排队。
e m p t y empty empty 我个人认为也可以理解为互斥信号量,表示座位需要互斥使用,这时 V ( e m p t y ) V(empty) V(empty) 就要写在顾客进程里。
大题13
题目
答案
可以联想到读者写者问题,那题读者用了计数器,这里我们类比一下
信号量设置
- 互斥信号量 m u t e x N t o S mutexNtoS mutexNtoS,由北向南的车辆互斥的访问用来统计由北向南车辆的数量的计数器
- 互斥信号量 m u t e x S t o N mutexStoN mutexStoN,由南向北的车辆互斥的访问用来统计由南向北车辆的数量的计数器
- 互斥信号量 m u t e x mutex mutex,两方向的车流互斥访问桥
semaphore mutexNtoS = 1;
semaphore mutexStoN = 1;
semaphore mutex = 1;
int numNtoS = 0;
int numStoN = 0;
NtoS()
{
P(mutexNtoS);
if (numNtoS == 0)
{
P(mutex);
}
numNtoS++;
V(mutexNtoS);
通过桥;
P(mutexNtoS);
numNtoS--;
if (numNtoS == 0)
{
V(mutex);
}
V(mutexNtoS);
}
StoN()
{
P(mutexStoN);
if (numStoN == 0)
{
P(mutex);
}
numStoN++;
V(mutexStoN);
通过桥;
P(mutexStoN);
numStoN--;
if (numStoN == 0)
{
V(mutex);
}
V(mutexStoN);
}
显然,这种写法容易造成饥饿!
大题14
题目
答案
- 不能成功,有可能2个线程都在临界区。因为没有保证进入函数的原子性。
- 会出现死锁。flag[0]为真后切换到线程1执行,flag[1]为真。这是二者执行下一条语句时都会被阻塞,产生死锁。
大题15
题目
答案
信号量设置
- 互斥信号量 m u t e x mutex mutex,初值为一,保证互斥访问箱子
- 同步信号量 f u l l 1 full1 full1,初值为零,表示箱子里车架的数量
- 同步信号量 f u l l 2 full2 full2,初值为零,表示箱子里车轮的数量
- 同步信号量 e m p t y empty empty,初值为 N N N,表示空闲的位置数
- 同步信号量 l e f t 1 left1 left1,初值为 N − 2 N-2 N−2,表示最多还可以放多少车架
- 同步信号量 l e f t 2 left2 left2,初值为 N − 1 N-1 N−1,表示留给车轮的空位置数量
semaphore mutex = 1;
semaphore full1 = 0;
semaphore full2 = 0;
semaphore empty = N;
semaphore left1 = N - 2;
semaphore left2 = N - 1;
工人1活动:
do
{
加工一个车架;
P(left1);
P(empty);
P(mutex);
车架放入箱子中;
V(mutex);
V(full1);
}while(1)
工人2活动:
do
{
加工一个车轮;
P(left2);
P(empty);
P(mutex);
车轮放入箱子中;
V(mutex);
V(full2);
}while(1)
工人3活动:
do
{
P(full1);
P(mutex);
箱中取出一个车架;
V(mutex);
V(empty);
V(left1);
P(full2);
P(full2);
P(mutex);
箱中取出两个车轮;
V(empty);
V(empty);
V(left2);
V(left2);
组装为一台车;
}while(1)
最开始写的时候没有设置信号量 l e f t 1 left1 left1 和 l e f t 2 left2 left2,主要是没有考虑到死锁的情况,如果工人1干活特别麻利,在工人2最多生产出来一个前,就已经把箱子填满了,那么工人二生产后也无法放入箱中,工人三也无法组装,产生死锁。
⛔️ 注意!
- 王道没有写互斥信号量,绝对是错误的!大家可以理一下逻辑,没有互斥,工人1和工人2是完全有可能同时进入临界区的,违背了基本原则。
- 我看的是22版的王道,到我看了下最新出的23版也没有修正这个错误,我觉得是很不应该的。说实话,王道错误真的是挺多的。
大题16
题目
答案
信号量设置
- 同步信号量 f u l l full full,初值为零,表示缓冲区有物品的数量
- 同步信号量 e m p t y empty empty,初值为一,表示缓冲区为空的数量
- 互斥信号量 m u t e x mutex mutex,初值为一
semaphore full = 0;
semaphore empty = 1;
semaphore mutex = 1;
P()
{
while (true)
{
生产物品;
P(empty);
P(mutex);
将产品放入缓冲区;
V(mutex);
V(full);
}
}
Q()
{
while (true)
{
P(full);
P(mutex);
取走产品;
V(mutex);
V(empty);
}
}
R()
{
if (empty == 1)
{
生产物品;
P(empty);
P(mutex);
将产品放入缓冲区;
V(mutex);
V(full);
}
if (full == 1)
{
P(full);
P(mutex);
取走产品;
V(mutex);
V(empty);
}
}
大题17
题目
答案
信号量设置
- 互斥信号量,初值为一,确保互斥访问 w a i t i n g waiting waiting 变量
- 同步信号量 f u l l full full,初值为 0 0 0,表示已经坐人的椅子数量
- 同步信号量 s e r v i c e service service,初值为零,用来实现顾客等待理发师的同步关系
错误示范
semaphore empty = n;
semaphore full = 0;
semaphore service = 0;
顾客
{
P(empty);
V(full);
P(service);
}
理发师
{
while (true)
{
P(full);
V(empty);
V(service);
理发;
}
}
这题和之前那道真题比较类似,但做的时候就发现题目中的无椅子可做就离开无法实现,按我的代码逻辑,是要阻塞在 e m p t y empty empty 排队等待的
正确答案
semaphore mutex = 1;
semaphore full = 0;
semaphore service = 0;
int waiting = 0;
顾客
{
P(mutex);
if (waiting < n)
{
waiting++;
V(mutex);
V(full);
P(service);
被理发;
}
else
{
V(mutex);
无座离开;
}
}
理发师
{
while (true)
{
P(full);
P(mutex);
waiting--;
V(mutex);
V(service);
理发;
}
}
注意这里的 V ( m u t e x ) V(mutex) V(mutex) 在if和else里都有,强调逻辑的严谨,避免死锁
大题18
题目
答案
明显又是读者写者问题的演变
信号量设置
- 互斥信号量 m u t e x mutex mutex,初值为一,3类观众互斥访问录像厅
- 互斥信号量 m u t e x 1 mutex1 mutex1,初值为一,看第一部电影的观众互斥访问变量 c o u n t 1 count1 count1
- 互斥信号量 m u t e x 2 mutex2 mutex2,初值为一,看第二部电影的观众互斥访问变量 c o u n t 2 count2 count2
- 互斥信号量 m u t e x 3 mutex3 mutex3,初值为一,看第三部电影的观众互斥访问变量 c o u n t 3 count3 count3
semaphore mutex = 1;
semaphore mutex1 = 1;
semaphore mutex2 = 1;
semaphore mutex3 = 1;
int count1 = 0;
int count2 = 0;
int count3 = 0;
P1()
{
P(mutex1);
if (count1 == 0)
{
P(mutex);
}
count1++;
V(mutex1);
看电影;
P(mutex1);
count1--;
if (count1 == 0)
{
V(mutex);
}
V(mutex1);
}
P2()
{
P(mutex2);
if (count2 == 0)
{
P(mutex);
}
count2++;
V(mutex2);
看电影;
P(mutex2);
count2--;
if (count2 == 0)
{
V(mutex);
}
V(mutex2);
}
P1()
{
P(mutex3);
if (count3 == 0)
{
P(mutex);
}
count3++;
V(mutex3);
看电影;
P(mutex3);
count3--;
if (count3 == 0)
{
V(mutex);
}
V(mutex3);
}
大题19
题目
答案
理论上说每次只能一辆自行车通过,所以整条路应该互斥访问,但由于 M M M 的存在,可以使左右双方向最多各有一辆车然后在这里错车
信号量设置
- 互斥信号量 T 2 N T2N T2N,初值为1,从T到N方向只允许一辆车进入
- 互斥信号量 N 2 T N2T N2T,初值为1,从N到T方向只允许一辆车进入
- 互斥信号量 L L L,初值为1,L这段路只允许一辆车进入,是为了保证如果从t到n方向的车骑得快的话,也必须在安全岛等待,而不能到L与另一方向的车阻塞在L。
- 互斥信号量 K K K,初值为1
semaphore T2N = 1;
semaphore N2T = 1;
semaphore L = 1;
semaphore K = 1;
T到N方向
{
P(T2N);
P(L);
从T进入L;
进入M;
V(L);
P(K);
从K到N;
V(K);
V(T2N);
}
N到T方向
{
P(N2T);
P(K);
从N进入K;
进入M;
V(K);
P(L);
从L到T;
V(L);
V(N2T);
}
大题20
题目
答案
信号量设置
- 同步信号量 f u l l full full,初始值 0 0 0,缓冲区的产品数
- 同步信号量 e m p t y empty empty,初始值 1000 1000 1000,缓冲区剩余可以放的产品数
- 互斥信号量 m u t e x mutex mutex,互斥访问缓冲区
- 互斥信号量 m u t e x _ c o n s u m e r mutex\_consumer mutex_consumer,保证一个消费者连续取10件产品,其他消费者才能取产品
semaphore full = 0;
semaphore empty = 1000;
semaphore mutex = 1;
semaphore mutex_consumer = 1;
Producer_i()
{
while (true)
{
P(empty);
P(mutex);
放产品到缓冲区;
V(mutex);
V(full);
}
}
Consumer_j()
{
while (true)
{
P(mutex_consumer);
for (int i = 0; i < 10; i++)
{
P(full);
P(mutex);
从缓冲区取走一件产品;
V(mutex);
V(empty);
}
V(mutex_consumer);
}
}
大题21
题目
答案
信号量设置
- 同步信号量 c l o s e close close,初值为零,先关车门再启动车辆
- 同步信号量 o p e n open open,初值为零,先停车再开车门
semaphore open = 0;
semaphore close = 0;
驾驶员
{
P(close);
启动车辆;
正常行车;
到站停车;
V(open);
}
售票员
{
关车门
V(close);
售票;
P(open);
开车门;
}
大题22
题目
答案
信号量设置
- 同步信号量 f u l l A fullA fullA,初值为 x x x,表示A的信箱中邮件数量
- 同步信号量 f u l l B fullB fullB,初值为 y y y,表示B的信箱中邮件数量
- 同步信号量 e m p t y A emptyA emptyA,初值为 M − x M-x M−x,表示信箱最多还可以放多少邮件
- 同步信号量 e m p t y B emptyB emptyB,初值为 N − y N-y N−y,表示信箱最多还可以放多少邮件
- 互斥信号量 m u t e x A mutexA mutexA,互斥访问信箱A
- 互斥信号量 m u t e x B mutexB mutexB,互斥访问信箱B
semaphore fullA = x;
semaphore fullB = y;
semaphore emptyA = M - x;
semaphore emptyB = N - y;
semaphore mutexA = 1;
semaphore mutexB = 1;
A
{
while (true)
{
P(fullA);
P(mutexA);
从A的信箱中取出一个邮件;
V(mutexA);
V(emptyA);
回答问题并提出一个新问题;
P(emptyB);
P(mutexB);
将新邮件放入B的信箱;
V(mutexB);
V(fullB);
}
}
B
{
while (true)
{
P(fullB);
P(mutexB);
从B的信箱中取出一个邮件;
V(mutexB);
V(emptyB);
回答问题并提出一个新问题;
P(emptyA);
P(mutexA);
将新邮件放入A的信箱;
V(mutexA);
V(fullA);
}
}
大题23
题目
答案
信号量设置
- 互斥信号量 m u t e x Y 1 mutexY1 mutexY1,初值为一,保证线程1和3互斥访问y
- 互斥信号量 m u t e x Y 2 mutexY2 mutexY2,初值为一,保证线程2和3互斥访问y
- 互斥信号量 m u t e x Z mutexZ mutexZ,初值为一,保证线程2和3互斥访问z
问:这里为什么不能只用一个信号量来使三个线程互斥访问变量 y y y?
答:实际上我们可以看出线程1和2是读者,为了最大限度地并发执行,这两个线程应该可以同时访问临界资源。而线程3既要读又要写,必须互斥
semaphore mutexY1 = 1;
semaphore mutexY2 = 1;
semaphore mutexZ = 1;
thread1
{
cnum w;
P(mutexY1);
w = add(x, y);
V(mutexY1);
}
thread2
{
cnum w;
P(mutexY2);
P(mutexZ);
w = add(y, z);
V(mutexZ);
V(mutexY2);
}
thread3
{
cnum w;
w.a = 1;
w.b = 1;
P(mutexZ);
z = add(z, w);
V(mutexZ);
P(mutexY1);
P(mutexY2);
y = add(y, w);
V(mutexY1);
V(mutexY2);
}
大题24
题目
答案
在哲学家问题中,有一种解决死锁的方法就是限制最多有 n − 1 n-1 n−1 位哲学家同时进餐,而这里的碗实际上就可以起到这种效果。
如果 m < n m<n m<n,那毫无疑问,可以达到限制效果
如果 m ≥ n m \ge n m≥n,我们可以形式上没收一些碗,使得信号量初值为 n − 1 n-1 n−1
信号量设置
- 互斥信号量 b o w l bowl bowl,初值为 m i n ( n − 1 , m ) min(n-1,m) min(n−1,m),用于协调哲学家对碗的使用
- 互斥信号量 c h o p s t i c k s [ n ] chopsticks[n] chopsticks[n],初值均设为 1 1 1,用于协调哲学家对筷子的使用
semaphore bowl;
semaphore chopsticks[n];
for (int i = 0; i < n; i++)
{
chopsticks[i] = 1;
}
bowl = min(n - 1, m);
CoBegin
while (true)
{
思考;
P(bowl);
P(chopsticks[i]);
P(chopsticks[(i + 1) % n]);
就餐;
V(chopsticks[i]);
V(chopsticks[(i + 1) % n]);
V(bowl);
}
CoEnd
大题25
题目
答案
这题就是简单的前驱问题,我想直接偷懒就写2个信号量,然后P两次即可。我觉得这样应该也没有错,但不敢保证,稳妥起见还是用王道的写法
信号量设置
- 信号量 S A C S_{AC} SAC,初值为零,控制A和C的执行顺序
- 以此类推
semaphore Sac = 0;
semaphore Sbc = 0;
semaphore Sce = 0;
semaphore Sde = 0;
A()
{
V(Sac);
}
B()
{
V(Sbc);
}
C()
{
P(Sac);
P(Sbc);
完成动作C;
V(Sce);
}
D()
{
V(Sde);
}
E()
{
P(Sce);
P(Sde);
完成动作E;
}
七、天道酬勤
王道书上出现了多次的一句话:考研复习的关键在于反复多次和全面,“偷工减料”是要吃亏的。