王道考研操作系统同步与互斥(王道大题详解)

操作系统

前言

image-20220224210659791

就看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 指令交换 lockkey 的内容,然后检查 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 S0,就会不断地测试,因此并未遵循 让权等待 的原则

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.利用信号量实现前驱关系

image-20220208200238710

上图是一个前驱图,信号量初始值均设为零。为了保证 S 1 → S 2 , S 1 → S 3 S_{1}\to S_{2},S_{1}\to S_{3} S1S2,S1S3 的前驱关系,设置信号量 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部分组成:

  1. 管程的名称
  2. 局部于管程内部的共享数据结构说明
  3. 对该数据结构进行操作的一组过程
  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.用管程解决生产者消费者问题

王道伪代码如下:

image-20220216160443290

右边是生产者消费者进程代码,可见管程封装程度之高。

五、经典同步问题

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倍速把咸鱼视频过一遍再看书,效率奇高。

image-20220222220125383

上图是加了互斥信号量的情况。那么为什么可以去掉?

原因在于本题缓冲区大小为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 → 写 者 1 → 读 者 2 读者1\to写者1\to读者2 112
  • 答:读者1正在读文件时,写者1来了,会被 r w rw rw 信号量阻塞,读者2来了,会被 w w w 信号量阻塞。在读者1读完撤离后,$ V(rw)$ 保证了写者1紧随其后运行,写者1撤离后, V ( w ) V(w) V(w) 保证读者2紧随其后运行
  1. 问: 写 者 1 → 读 者 1 → 写 者 2 写者1\to读者1\to写者2 112
  • 答:写者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 04,哲学家 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);
    }
}

下图是王道书中供应者的相关代码

image-20220223095200412

我觉得是有问题的。我的改动地方就是“任意两种材料放在桌子上”这句话的位置。按照王道的逻辑,我 V V V 完后被切到对应的那个吸烟者进程,那个进程就可以进行卷烟吸烟的操作。但按照逻辑来说,你材料实际上并没有放到缓冲区,这是于事实相矛盾的!

还有要强调的是这里供应者的 P V PV PV 顺序可不可以换。我认为是可以的,当然前提是改 f i n i s h finish finish 初值为1。

六、王道习题详解

大题1
题目

image-20220223110444971

答案

当共享资源用共享数据结构表示时,资源管理程序可用对该数据结构进行操作的一组过程来表示,如资源的请求和释放过程request和release。。把这样一组相关的数据结构和过程一并归为管程。Hansan为管程所下的定义是:“一个管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据。”由定义可知,管程由三部分组成:

1)局部于管程的共享变量说明。

2)该数据结构进行操作的一组过程。

3)对局部于管程的数据设置初始值的语句;此外,还需为该管程赋予一个名字。

管程的引入是为了解决临界区分散所带来的管理和控制问题。在没有管程之前,对临界区的访问分散在各个进程之中,不易发现和纠正分散在用户程序中的不正确使用,V操作等问题。管程将这些分散在各进程中的临界区集中起来,并加以控制和管理,管程一次只允许一个进程进入管程内,从而既便于系统管理共享资源,又能保证互斥。

大题2
题目

image-20220223110521169

答案

1)是互斥关系,同一本书只能被一名学生借阅,或任何时刻只能有一名学生借阅一本书。

2)是互斥关系,篮球是互斥资源,只可被一个队伍获得。

3)是同步关系,一个工序完成后开始下一个工序。

4)是同步关系,生产商品后才能消费。

大题3
题目

image-20220223110544436

答案

信号量设置

  • 互斥信号量 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
题目

image-20220223110604667

image-20220223110618878

答案

不能, x x x 作为临界资源没有被互斥访问

直接上答案

image-20220223110956289

大题5
题目

image-20220223111019582

答案

这题还是挺坑的,没考虑全,因为甚至在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
题目

image-20220223112354699

答案

对于条件二显然需要同步信号量。这个要求如何转化?假设信号量 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 M1 ,表示产品A还可以放的数量
  • 同步信号量 S b S_b Sb,初值为 N − 1 N-1 N1 ,表示产品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
题目

image-20220224191603910

答案

信号量设置

  • 同步信号量 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
题目

image-20220224195125476

答案

信号量设置

  • 同步信号量 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
题目

image-20220224200813665

答案

信号量设置

  • 互斥信号量 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
题目

image-20220224202914887

答案

信号量设置

  • 互斥信号量 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
题目

image-20220225112317041

答案

理一下关系,数据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);
}

发现写出来和王道答案不一样,我又推了一下,感觉自己的也没问题

image-20220225120635184

我是在 y = a ∗ b y=a*b y=ab 这一计算结束后才允许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
题目

image-20220225121915218

答案

信号量设置

  • 同步信号量 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
题目

image-20220227175951116

image-20220227180013198

答案

可以联想到读者写者问题,那题读者用了计数器,这里我们类比一下

信号量设置

  • 互斥信号量 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
题目

image-20220227182007058

答案
  1. 不能成功,有可能2个线程都在临界区。因为没有保证进入函数的原子性。
  2. 会出现死锁。flag[0]为真后切换到线程1执行,flag[1]为真。这是二者执行下一条语句时都会被阻塞,产生死锁。
大题15
题目

image-20220227192039400

答案

信号量设置

  • 互斥信号量 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 N2,表示最多还可以放多少车架
  • 同步信号量 l e f t 2 left2 left2,初值为 N − 1 N-1 N1,表示留给车轮的空位置数量
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
题目

image-20220301123851773

答案

信号量设置

  • 同步信号量 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
题目

image-20220301125352146

image-20220301125400184

答案

信号量设置

  • 互斥信号量,初值为一,确保互斥访问 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
题目

image-20220301132524722

答案

明显又是读者写者问题的演变

信号量设置

  • 互斥信号量 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
题目

image-20220301140104681

答案

理论上说每次只能一辆自行车通过,所以整条路应该互斥访问,但由于 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
题目

image-20220301141743876

答案

信号量设置

  • 同步信号量 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
题目

image-20220301143501786

答案

信号量设置

  • 同步信号量 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
题目

image-20220301144342104

image-20220301144352457

答案

信号量设置

  • 同步信号量 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 Mx,表示信箱最多还可以放多少邮件
  • 同步信号量 e m p t y B emptyB emptyB,初值为 N − y N-y Ny,表示信箱最多还可以放多少邮件
  • 互斥信号量 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
题目

image-20220301150125883

答案

信号量设置

  • 互斥信号量 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
题目

image-20220301153249456

答案

在哲学家问题中,有一种解决死锁的方法就是限制最多有 n − 1 n-1 n1 位哲学家同时进餐,而这里的碗实际上就可以起到这种效果。

如果 m < n m<n m<n,那毫无疑问,可以达到限制效果

如果 m ≥ n m \ge n mn,我们可以形式上没收一些碗,使得信号量初值为 n − 1 n-1 n1

信号量设置

  • 互斥信号量 b o w l bowl bowl,初值为 m i n ( n − 1 , m ) min(n-1,m) min(n1,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
题目

image-20220301155519818

答案

这题就是简单的前驱问题,我想直接偷懒就写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;
}

七、天道酬勤

王道书上出现了多次的一句话:考研复习的关键在于反复多次和全面,“偷工减料”是要吃亏的。

  • 8
    点赞
  • 90
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值