操作系统【三】

(4)And型信号量

上述进程互斥问题,即多个进程共享一个临界资源。

假设:两个进程A和B,共享数据D和E,为其分别设置互斥信号量Dmutex和Emutex,初值都设为1。

1
2
3
4
5
6
Process  A:
wait(Dmutex);
wait(Emutex);
Process B:
wait(Emutex);
wait(Dmutex);

若执行:

1
2
3
4
Process  A: wait(Dmutex);    //Dmutex=0
Process B: wait(Emutex); //Emutex=0
Process A: wait(Emutex); //Emutex=-1 A阻塞
Process B: wait(Dmutex); //Dmutex=-1 B阻塞

所以共享的资源越多,死锁的可能越大

AND同步机制的基本思想:将进程在整个运行过程中需要的所有资源,一次性全部分配给进程,待进程使用完后再一起释放。

只要尚有一个资源未能分配给进程,其他所有可能为之分配的资源,也不分配给它。即对临界资源的分配采取原子操作。

1
2
3
4
5
6
7
8
9
10
Swait(S1, S2, …, Sn)
{ while(1)
{ if (S1>=1 && …&& Sn>=1)
{ for (i=1; i<=n; i++) Si -- ;
break;
}
else
Place the process in the waiting queue associated with the first Si found with Si <1,and set the progress count of this process to the beginning of Swait operation
} //当某些资源Si不能被满足时,调用进程进入第一个资源Si没能满足的等待队列中,进程阻塞
}
1
2
3
4
5
6
7
Ssignal(S1, S2, …, Sn)
{ while(1)
{ for i:=1 to n do Si ++ ;
Remove all the process waiting in the queue associated with Si into the ready queue
}
//将等待队列中能满足所有资源需求的进程转移到就绪队列中准备执行
}

(5)信号量集

一般信号量集

记录型和AND信号量机制每次只能获得或释放一个单位的资源,相对低效 引入信号量集的思想每次可以分配若干个资源,但是每次分配前必须测试资源数量,看当前资源量是否大于可以分配资源的下界值ti ,当资源量低于下界值时便不予以分配。

Swait(S1, t1, d1, …, Sn, tn, dn)

Si 为资源信号量,di 为资源需求值,ti 为可分配资源的下限值

1
2
3
4
5
6
7
Swait(S1, t1, d1, …, Sn, tn, dn)
{ if( S1>= t1 && … && Sn>= tn)
for (i=1 ; i<=n ; i++) Si= Si - di ;
else
Place the executing process in the waiting queue of the first Si with Si < ti and set its program counter to the beginning of the Swait Operation
//当某些资源不能被满足时,调用进程进入第一个资源小于ti的信号量的等待队列中,调用进程阻塞
}
1
2
3
4
5
Ssignal(S1, d1, …, Sn, dn)
{ for (i=1 ; i<=n ; i++)
Si= Si +di ;
Remove all the process waiting in the queue associated with Si into the ready queue//将等待队列中能满足所有资源需求的进程转移到就绪队列中准备执行
}

从P0 进程的角度来说,执行 P 操作的时候,不仅要传多个信号量进去,而且每一个信号量还有配套的 t 和 d,分别表示“最小必须满足值”和“分配数目”,也就是说,每一个资源 Si 的数目都必须大于等于对应的 ti,才能将其中的 di 个分配给某个进程;同理,执行 V 操作的时候,也是要传多对 Si,di 进去,让每一种资源都得到一定的释放,同时唤醒对应阻塞队列中的进程。

一般信号量集的几种特殊情况:

  • Swait(S, d, d),只有一个信号量S,允许每次申请d个资源,若现有资源数少于d,不予分配。

  • Swait(S, 1, 1),蜕化为一般的记录型信号量

    • 若 S>=1时,S=S-1 >=0,允许进程进入临界区
    • 若 S<1 时,S=S-1 <0,临界区中有进程在执行,调用进程必须阻塞,进入等待队列
  • Swait(S, 1, 0),当S>=1时,允许多个进程进入某特定区,当S变为0后,阻止任何进程进入特定区,相当于可控开关。

  • 一次只要申请一种资源,而且一次申请资源数为一个的使用记录型信号量

  • 一次申请多种资源,每种资源一次申请一个的使用AND信号量

  • 一次申请多种资源,每种资源一次申请多个的使用信号量集

(6)信号量的应用

  1. 利用信号量实现进程互斥

为使多个进程互斥的访问某临界资源,须为该资源设置一互斥信号量mutex,并设其初始值为1,然后将各进程访问资源的临界区置于wait(mutex)和signal(mutex)之间即可。

1
2
3
4
5
6
7
8
9
10
11
semaphore mutex = 1;
P0(){
P(mutex)
critical section
V(mutex)
}
P1(){
P(mutex)
critical section
V(mutex)
}

2.信号量实现进程同步

在前面,我们一直用大量的篇幅解释进程互斥的实现,那么如何实现进程同步呢?也就是说,多个进程一起完成某项任务的时候,如何确保它们按照一定的先后顺序有秩序地执行呢?

实际上,信号量机制也可以很好地实现进程同步。它的核心是三个关键步骤:

  • 设置信号量初始值为 0
  • 在”前操作“之后执行 V(S)
  • 在”后操作“之前执行 P(S)

先来解释一下原理,即:为什么这样就可以保证两个操作的前后顺序呢?首先我们先记住一点,0 是一个非常关键的”分水岭“,大于 0 的时候不会发生阻塞,小于 0 则会发生阻塞。

我们要确保”前操作“在前面,”后操作“在后面,实际上只要做到三件事:V 在”前操作“后面、P 在”后操作“前面、V 在 P 前面。第一个和第二个条件都是可以通过实际书写代码来做到的,而要达到第三个条件 —— V 在 P 前面,就有必要让信号量初始值为 0,因为一旦初始值为 0,则每当 P 想要”违规“抢先于 V 执行的时候,都会由于首先执行信号量自减的操作而导致当前所在进程进入阻塞队列 ,也就是说:

==P 先于 V 执行 —> P 所在进程会被阻塞 —> ”后操作“始终无法执行==

所以,在这种情况下,就只能转而执行 V 所在的进程了。在这个进程里,由于 V 在”前操作“后面,所以一定是”前操作“执行完了再去执行 V。而执行 V 就会自增信号量,同时唤醒对方进程,对方进程再去顺序执行 P 操作 —— 虽然此时信号量又自减,但是是在加一的基础上自减,所以并不会导致再次阻塞,所以 P 执行完后就顺序执行”后操作“。由此,我们确保了两个操作一定是严格按照先后顺序执行的。

3.信号量实现进程前驱关系

其实这种情况就是把多个同步问题结合起来,对于每一对前驱关系来说,都有属于本关系的信号量,所以我们仍然是可以用信号量机制来实现的。

5、经典进程的同步问题

(1)生产者—消费者问题

生产者 — 消费者问题描述的是一个对产品生产和消费的过程:首先,对于生产者,如果缓冲区没满,它就会生产产品并将其放入缓冲区,如果缓冲区已满,它就会进行等待;对于消费者,如果缓冲区不空,它就会取走产品并释放缓冲区,如果缓冲区已空,它就会进行等待。

对于这个问题,不难看出有两个进程,一个是生产者进程,一个是消费者进程;同时有一个互斥关系,在同一时间内,只能有一个进程访问同一个缓冲区,要么放入产品,要么取走产品;同时有两个同步关系,一个指的是:必定是先生产产品,后取走产品,另一个指的是:必定是先释放缓冲区,后使用缓冲区。

用数组表示具有n个缓冲区的缓冲池

输入指针in指示下一个可投放产品的缓冲区,输出指针out指示下一个可从中获取产品的缓冲区,初值均为0。

缓冲池组织为循环缓冲,输入指针 加1表示为in:=(in+1)% n,输出指针 加1表示为out:=(out+1)% n,当 in==out表示缓冲池空;(in+1)% n= =out时表示缓冲池满

1
2
item buffer[n] ;
int in=0, out=0;

在生产者和消费者之间的公用缓冲池buffer[n]是生产者和消费者共享的临界资源,这个临界资源需要两个进程互斥的访问。可以设置两个信号量来表示缓冲区满和空两种情况,来控制生产者和消费者的进度情况。

==注意:==

==1。每个程序中用于实现互斥的wait(mutex)和signal(mutex)必须成对地出现。==

==2。对资源信号量empty和full的wait和signal操作,同样需要成对地出现,但处于不同的程序中。==

==3。在每个程序中的多个wait操作顺序不能颠倒。应先执行对资源信号量的wait操作,再执行对互斥信号量的wait操作,否则可能引起进程死锁。==

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    producer(){                         consumer(){
    while(1){ while(1){
    生产产品 P(mutex)
    P(mutex) P(full)
    P(empty) 从缓冲区中取走产品
    把产品放入缓冲区 V(mutex)
    V(mutex) V(empty)
    V(full) 使用产品
    } }
    } }

这个写法的问题在于,还没有先检查缓冲区是否没满或者是否非空,就强行进行互斥的“上锁”。假如还是按照前面的流程,一开始处理机在 consumer 这里,那么 consumer 实际上在没有检查缓冲区是否非空的情况下就直接“上锁”了,这导致它在 P(full) 这一步的时候被卡住,只能等待,而在之后切换到 producer 的时候,正常情况下他应该生产可供对方消费的产品,但是由于对方已经上了锁,所以 producer 在 P(mutex) 这一步也被卡住了,只能进行等待。这也就是说,producer 等待 consumer 释放临界区,而 consumer 等待 producer 使用缓冲区,双方陷入循环等待,造成了“死锁”。

另一种情况,我们也可以设想一开始处理机是在 producer 这里,那么是不是不会导致“死锁”呢?并不是。事实上,按照这种情况正常用完全部缓冲区之后,如果处理机还是在 producer 这里,那么 producer 在检查缓冲区是否已满之前也会将自己提前上锁,在 P(empty) 这一步陷入等待,之后即使进程切换到 consumer 这里,它也会因为对方提前上锁,而导致自己在 P(mutex) 这一步陷入等待。也就是说,这不过是前面那种”死锁“现象的翻版。

==进程的同步与互斥分析==

  • 当并发的多个进程有直接制约关系时形成进程的同步

  • 进程的同步:一个进程的执行需要另外一个或者多个进程的配合。

    • 这里的同步主要是协调的意思。
  • 当并发的多个进程有间接制约关系时形成进程的互斥

  • 进程的互斥:多个进程有序、有效的竞争系统的共享资源。

  • 进程互斥中用于实现互斥的P操作和V操作必须成对地出现

  • 进程同步的P操作和V操作,同样需要成对地出现,但处于不同的程序中

  • 在每个程序中的多个P操作顺序不能颠倒。

  • 应先执行对同步信号量的P操作,再执行对互斥信号量的P操作,否则可能引起进程死锁

利用AND信号量解决生产者——消费者问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
semaphore mutex=1, empty=n, full=0;
item buffer[n] ;
int in=0, out=0;
void proceducer( )
{
do
{
produce an item in nexp;

Swait(empty, mutex);
buffer[in]:=nexp;
in:=(in+1) % n;
Ssignal(mutex, full);
}
while(1);
}
void consumer ( )
{
do
{
Swait(full, mutex);
nextc:=buffer[out];
out:=(out+1) % n;
Ssignal(mutex, empty);
consume the item in nexc;

}
while(1);
}

(2)哲学家进餐问题

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

可见:相邻两位不能同时进餐;最多只能有两人同时进餐。

首先,五个哲学家对应了五个进程,然后在同一个时间段内,对于同一支筷子,只能有一个哲学家去使用它,所以筷子是一种临界资源,我们用互斥信号量 chopstick = 1 表示。鉴于这里有五支筷子,所以我们准备一个互斥信号量数组 chopstick[5] = {1,1,1,1,1}。另外,由于任何一个哲学家都只可能拿离自己最近的左右筷子,所以为了加以区分,我们需要给哲学家和筷子进行编号。对于哲学家,从 0 到 4 进行编号,由于哲学家按照圆桌首尾连接,所以某个哲学家左右两边的筷子编号与自己本身的编号相关。以哲学家 i 为例,它左边的筷子编号是 i。右边则是 (i+1)%5,如下图所示:

对每一个哲学家进程来说,它都只能拿起左右两边的筷子,并且一定是吃饭前拿筷子,吃饭后放下筷子,所以初步的伪代码是这样的(这里忽略思考的过程):

1
2
3
4
5
6
7
8
9
Pi(){
while(1){
P(chopstick[i])
P(chopstick[(i+1)%5])
eat()
V(chopstick[i])
V(chopstick[(i+1)%5])
}
}

如果哲学家 0 号拿起左右筷子后,进程切换到哲学家 1 号,那么哲学家 1 号是会被阻塞的,所以这样写可以保证相邻的哲学家中有一个可以吃饭。但是,如果是拿起左筷子,之后进程切换到 1 号,那么 1 号也会拿起自己的左筷子,以此类推,直到 4 号也拿起自己的左筷子。接着,进程来到 0 号,这时候,0 号会发现自己已经没有右筷子可以拿了(作为 1 号的左筷子),于是 0 号被阻塞;同理,1 号也没有右筷子,所以他也被阻塞……以此类推,由于所有的哲学家都只有左筷子,他们的右筷子都在其他人手里,这导致了所有的哲学家都被阻塞,等待着另一个哲学家释放筷子。但实际上,没有任何哲学家能够吃饭,因此没有人可以释放筷子,这使得这些哲学家都陷入无限的等待中,造成“死锁”的发生。

解决这个问题有三个方法。

  • 至多只允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够进餐,并在用毕后释放出他用过的两只筷子,从而使更多的哲学家能够进餐。

    • 准备一个互斥信号量 count,但是这个信号量初值不再是 1 ,而是 4,表示最多允许四个哲学家参与过程。当 count 减少到 -1 的时候,就不能再让哲学家进来了,因此可以保证最多只有四个哲学家。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      Pi(){
      while(1){
      P(count)
      P(chopstick[i])
      P(chopstick[(i+1)%5])
      eat()
      V(chopstick[i])
      V(chopstick[(i+1)%5])
      V(count)
      }
      }
  • 仅当哲学家的左右两只筷子均可用时,才允许他拿起筷子进餐。

    • 利用AND信号量机制解决哲学家进餐问题

      1
      2
      3
      4
      5
      6
      7
      8
      semaphore chopstick[5] ={1,1,1,1,1};
      Process i
      do{
      think;
      Swait(chopstick[ ( i +1) % 5] , chopstick[ i ] );
      eat;
      Ssignal(chopstick[ ( i +1) % 5] , chopstick[ i ] );
      }while[TRUE];
    • 利用互斥性,增加信号量用于互斥关系

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      Pi(){
      while(1){
      P(mutex)
      P(chopstick[i])
      P(chopstick[(i+1)%5])
      V(mutex)
      eat()
      V(chopstick[i])
      V(chopstick[(i+1)%5])
      }
      }
  • 规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子;偶数号哲学家则相反。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    Pi(){
    while(1)
    {
    if(i%2 == 0) // 偶数哲学家,先右后左。
    {
    P(chopstick[(i + 1)%5]) ;
    P(chopstick[i]) ;
    eat();
    V(chopstick[(i + 1)%5]) ;
    V(chopstick[i]) ;
    }
    else //奇数哲学家,先左后右。
    {
    P(chopstick[i]) ;
    P(chopstick[(i + 1)%5]) ;
    eat();
    V(chopstick[i]) ;
    V(chopstick[(i + 1)%5]) ;
    }
    }
    }

(3)读者—写者问题

有读者和写者两组进程,它们共同访问同一个文件。对于读者,它可以与多个读者共同读取文件(因为不会修改到文件);对于写者,它不能与其他任何进程共同访问文件(如果另一进程是写,则可能覆盖同一内容;如果是读,则可能修改到要读的内容)。也就是说,这里的互斥问题是读写互斥的问题,但与之前不同的是,除了实现读写、读读的互斥,我们还要实现读读的“不互斥”。

首先准备一个信号量 rw = 1 表示当前是否有进程在访问文件(注意一开始是没有这样的进程的,1 表示的不是进程数目,仅仅是使用互斥信号量时习惯上给定的初始值,这个看 wait 的代码就能理解了)。

在不考虑“读读不互斥”的情况下,我们的伪代码是这样的:

1
2
3
4
5
6
7
Writer(){               Reader(){
while(1){ while(1){
P(rw) P(rw)
写文件 读文件
V(rw) V(rw)
} }
} }

这个代码可以实现读写互斥,但显然无法实现“读读不互斥”,因为每个读进程之间也会受到 rw 的影响,使得最后只能有一个读进程访问文件。于是我们考虑从读进程入手,做一些改进。这里读和读不能同时进行的本质原因在于,所有的读进程都会经历“检查并上锁”这个步骤,而一个读进程进入后就会马上检查并上锁,导致另一个也想要进入的读进程被阻塞,所以我们考虑:能不能不要让所有的读进程都经历“检查并上锁”这一步骤?也就是说,某些进程可以跳过 P 操作,直接进入临界区,这样一来,这些进程就不存在在 P 操作这里被阻塞的可能性。

什么样的进程可以跳过 P 操作呢?就是中间的那些读进程。因为一开始肯定要有读进程上锁、最后肯定要有读进程解锁,所以上锁和解锁的任务交付给第一个和最后一个进程,而中间的那些进程来去自如,只需要负责读文件,不需要参与上锁和解锁。为了区分读进程的次序,我们准备一个 count = 0 的变量,它表示的是当前有多少个读进程正在访问文件。然后在读文件的前后,我们分别对 count 进行加一和减一的操作,每次读文件开始之前 count 会加一,所以在此之前如果变量为 0 ,说明当前读进程是第一个读进程;同理,每次读文件之后 count 会减一,所以在此之后如果变量为 0 ,说明当前读进程是最后一个读进程;

此时伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
Reader(){
while(1){
if(count==0)
P(rw)
count++
读文件
count--
if(count==0)
V(rw)
}
}

但是这样会产生一些问题。比方 1 号读进程首先进入并上锁,然后在 P 操作之后、count 加一变成 1 之前,进程切换到 2 号读进程,那么 2 号读进程就会卡在 P 操作这个地方,陷入阻塞,显然这时候无法实现我们想要的“读读不互斥”;又比方说,1 号读进程在 count 减一变成 0 之后、释放 rw 之前,进程切换到了 2 号读进程,那么 2 号同样又会被卡在 P 操作这里。所以我们还要进行改进。

问题其实就出在,对 count 的检查和赋值不是一个原子操作,这导致的结果是,如果在检查和赋值之间的空隙,进程发生切换,则必然会使得另一进程陷入阻塞。那么能不能让这两个操作一气呵成呢?事实上,可以把 count 当作是一个互斥访问的资源,对 count 的访问是互斥的,也就说明一个时间段内只能有一个读进程去访问它,即使这个过程中切换到了其它进程,那个进程也会被阻塞,从而保证只有一个进程可以访问 count,而这个访问就是检查和赋值,这种情况下,检查和赋值一定是不会被中断的。

准备一个互斥信号量 mutex = 1 表示对 count 的互斥访问,将检查和赋值封装在一个 PV 操作里。伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Reader(){
while(1){
P(mutex)
if(count==0)
P(rw)
count++
V(mutex)
读文件

P(mutex)
count--
if(count==0)
V(rw)
V(mutex)
}
}

现在我们再来跑一下过程。假设还是 1 号读进程运行到 P 操作的时候,进程切换到了 2 号读进程,那么由于互斥信号量 mutex 的存在,导致 2 号进程进入了 mutex 对应的阻塞队列 —— 是的,这时候看起来 2 号进程还是被阻塞了,不过我们要关注到的是,阻塞它的信号量是 mutex,不是 rw。这意味着,在进程重新切换回 1 号进程的时候,1 号进程一旦执行了 V(mutex),就可以将 2 号进程唤醒并送到就绪队列了。也就是说,尽管 2 号进程还是经历了“阻塞”这个过程,但是这个过程只是为了确保 1 号进程检查和上锁两个操作的原子性,一旦操作完成,2 号进程马上就被唤醒了。而之前那种情况不同,之前的情况是,导致 2 号进程被阻塞的是信号量 rw,除非 1 号进程读完后释放,否则 2 号进程会一直处于阻塞状态。这就是说,2 号进程永远不可能与 1 号进程同时读文件,但是改进后是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
semaphore rmutex =1, wmutex= 1;
int Readcount =0;
void Reader()
{
do
{
wait(rmutex);
//互斥访问读者数量
if Readcount==0
wait(wmutex);
//如果当前读者数为0则测试是否有写者
Readcount =Readcount +1;
//若没有则读者数加1
signal(rmutex);
//修改读者数量过程结束,释放读者信号量

perform read operation

wait(rmutex);
//读操作结束,互斥访问读者数量,准备修改读者个数
Readcount =Readcount -1;
if Readcount==0 //如果当前读者数为0则释放信号量 signal(wmutex); // wmutex允许写者进行写操作,
signal(rmutex);
//释放信号量rmutex允许其它读者修改读者数
}
while(TRUE);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Writer () 
{
do{
wait(wmutex);
//测试信号量wmutex的值确定是否可以进行写操作
perform write operation;
//执行写操作
signal(wmutex);
//写操作结束释放信号量
}while(TRUE)
}
void main()
{
cobegin
reader();
writer();
coend
}

但随之而来的又是另一个问题, “读写不公平”。也就是说,这样的代码本质上是对读进程更有利的。

因为对读进程来说,一旦第一个读进程进来了,中间即使穿插再多的读进程,也都是允许的,他们根本不受到 rw 这个“锁”的限制;而对于写进程,它的运气就没这么好了,写进程只要想进来,就必须通过 rw 这个“锁”,而这个“锁” 实际上又掌握在最后一个读进程手里 —— 这就是说,万一读进程源源不断进来,始终轮不到最后一个读进程解锁,那么写进程就只能陷入无尽的等待了。

既然 rw 这把锁无法做到公正对待每一个进程,那我们就考虑在外层加一把“更大、更公正的锁”。也就是说,所有的进程,无论读还是写,无一例外必须通过这把“锁”的检查。为此,我们准备一个新的互斥信号量 w = 1,并将 Writer 和 Reader 的一些关键操作封装在 w 的一对 PV 操作里。此时,伪代码如下:

img

我们来跑一下流程。假设首先来到 1 号读进程,那么它就会执行 P 操作上锁,这个过程中即使有写进程想进来,也会被送到 w 对应的阻塞队列。在 1 号读进程执行到 V 操作之后,写进程才会被唤醒并送到就绪队列,之后就轮到写进程执行了,而写进程虽然通过第一个 P 操作,但是被卡在了第二个 P 操作(读进程尚未释放 rw),所以他来到了 rw 对应的阻塞队列。

注意!重点来了,如果这时候 2 号读进程也想要访问文件,那么在以前,它是不需要通过任何检查就可以直接来读文件的,并且直到 2 号读进程释放 rw 之后,写进程才能真正来执行写文件的操作。但是现在由于我们加了一把“更大的锁”,导致 2 号进程也必须先通过 w 的检查,而由于写进程抢先在他之前上了锁,所以 2 号读进程被送到了 w 对应的阻塞队列。也就是说,现在的情况是:写进程等着 1 号读进程释放 rw,而 2 号读进程等着写进程释放 w,1 号读进程是让一切正常进行下去的关键。在处理机又来到 1 号读进程并执行 V(rw) 之后,写进程从 rw 的阻塞队列被唤醒,继续往下执行写文件的操作。而在写进程真正执行完之后,w 才能得到释放,由此又唤醒了 w 阻塞队列中的 2 号读进程,2 号读进程来到处理机运行。

如果换一种情况,是按照 写者 — 读者 — 写者的顺序,那么由于读者在第二个写者之前,所以是读者作为阻塞队列队头,第二个写者则次之,在后续执行过程中,根据队列“先进先出”的原则,也会是读者先于第二个写者访问文件。

也就是说,实际上谁先到、谁就在后续过程中先被执行(而不是像之前那种情况,无论写进程先还是后,读进程都可以“无视规则”抢先一步执行)。由此,我们就实现了“读写公平”。

-----------------------本文结束 感谢阅读-----------------------
坚持原创技术分享,您的支持将鼓励我继续创作!恰饭^.^~