0.关注博主有更多知识
操作系统入门知识合集
目录
0.关注博主有更多知识
4.1进程概念
4.1.1进程基本概念
思考题:
4.1.2进程状态
思考题:
4.1.3进程控制块PCB
4.2进程控制
思考题:
4.3线程
思考题:
4.4临界资源与临界区
4.4.1临界资源与临界区
思考题:
4.4.2锁机制
4.5同步和P-V操作
4.5.1同步和互斥的概念
4.5.2P-V操作概念
4.5.3P-V操作解决互斥问题
思考题:
4.5.4P-V操作解决同步问题
思考题:
4.5.5经典互斥与同步问题
4.1进程概念
4.1.1进程基本概念
描述和管理程序的"运行过程"称为进程。在Windows中可以打开任务管理器查看进程。
进程定义:
进程是程序在某个数据集合上的一次运行活动。注意抠字眼,进程是运行活动,前提是程序在某个数据集合上,这个数据集合就是软、硬件环境,被多个进程共享的环境,一个进程对应一次运行活动。
进程的特征:
1.动态性:进程是程序的一次执行过程,动态的产生,动态的消亡。
2.并发性:进程同其他进程一起向前推进。
3.异步性:每个进程按照自己各自的速度向前推进。
4.独立性:进程是操作系统分配资源和调度CPU(没了解线程之前暂时这么理解)的基本单位。
进程与程序的区别:
1.动态与静态:进程是动态的,它是程序的一次执行过程;程序是静态的,它是一组指令的有序集合。
2.暂存与长存:进程是暂存的,它在内存上短暂驻留;程序是长存的,它在磁盘或者其他介质上长期保存。
3.程序和进程的对应关系:一个程序可能有多个进程(一个程序运行多次就会产生多个进程)。
进程的类型:
1.按使用资源的权限分为系统进程和用户进程:系统进程指与操作系统内核相关的进程;用户进程指运行在用户态的进程。
2.按对CPU的依赖性划分为偏CPU进程和偏I/O进程:偏CPU进程指的是计算密集型进程;偏I/O进程指的是与用户交互频率较高的进程。
3.......
思考题:
1.进程具有异步性,即每个进程不考虑其他进程的运行速度,按自己的逻辑往后运行。那么这个异步的特点,对于进程来说是优点还是缺点?
当然是进程的优点,异步性能够提高CPU和I/O设备的利用率。进程在运行时根本不需要考虑其他进程运行的怎么样,只需要按照自己的逻辑往后运行即可。
4.1.2进程状态
进程的状态分为:
1.运行状态(Running):进程已经占有CPU,并且在CPU上运行。
2.就绪状态(Ready):具有运行条件但没有CPU而暂时不能运行,处于就绪态的进程只要占有CPU便立马可以运行。
3.阻塞状态(Block):进程因为等待某项服务的完成或者信号而不得不停下来,例如调用系统调用等待执行结果、I/O操作操作、等待合作进程的信号......
进程状态的变迁:
进程的状态可以根据一定的条件相互转换。
注意:我并没有标注可以从阻塞态到运行态、就绪态到阻塞态的转换。
实际上不同的操作系统有不同的进程状态,某些操作系统甚至具有新建态(new)和终止态(terminate):
Linux的进程状态:
1.可运行态:Linux没有就绪态,它把占有CPU的进程和处于就绪队列的进程的状态统称为可运行态。
2.阻塞态:Linux分为浅度阻塞和深度阻塞。浅度阻塞的进程可以被其他进程的信号或者时钟唤醒,反之深度阻塞的进程则不能。
3.僵尸态:进程终止运行时所处的状态,处于这个状态的进程会释放大部分资源。
4.挂起态:当调试程序时这个进程就处于挂起态。
思考题:
1.操作系统中为何没有"阻塞到运行"和"就绪到阻塞"这样的状态迁移?
进程调度是操作系统控制的,进程本身不具有调度的能力。当处于阻塞态的进程等待的服务完成后它就具备了运行的条件,根据操作系统对各个状态的定义,就必须放入就绪队列中等待操作系统的调度;进程处于阻塞态的前提是请求服务或者访问I/O,这本身就是一条指令,它需要被CPU执行,而处于就绪态的进程并没有占有CPU,所以不会执行这条指令。
4.1.3进程控制块PCB
进程控制块(Process Control Block):是描述进程状态、资源和相关进程关系的数据结构;PCB是进程的标志;创建进程时创建PCB,进程撤销时撤销PCB。
所以可以把进程的概念重新划分一下:进程=程序+PCB。
PCB数据结构如何用C语言实现的话,它就是一个struct结构体,里面包含了进程状态、资源等绝大部分属性集合。也就是说操作系统在调度进程的时候,不需要调度程序本身的代码和数据,而是调度PCB即可。
PCB中的基本成员:
Linux的进程控制块tast_struct(源码就不演示了):
和进程标识相关的成员变量:
1.Linux进程的标识:tast_struct有一名为PID的成员,它标识当前进程的唯一标识符;PPID表父进程的唯一标识符;PGID表示进程组的唯一标识符。
2.Linux进程的用户标识:UID成员表示用户ID(可以用来区分哪个用户创建的进程),GID表示用户组ID。
进程切换:
实际上我们可以回想一下中断的处理过程:保护现场、处理中断程序、恢复现场......进程切换可以从这个角度理解。
1.进程的上下文:在PCB有一Context字段描述上下文,上下文即表示进程的运行环境,通常与CPU有关(CPU上与当前进程有关的寄存器和寄存器里面的内容)。
2.进程切换过程:进程切换发生在时间片轮转结束时,我们可以把它看成中断。进程换出CPU时需要把上下文压入栈,要换入的进程需要把上下文从栈上放到CPU里面去。
4.2进程控制
进程控制概念:在进程的生命周期期间,操作系统对进程的全部控制行为。
进程会发生状态的转换,而这个转换不是进程本身完成的,而是操作系统对进程进行控制,从而让进程发生转换。典型的控制行为有四个:创建进程、阻塞进程、撤销进程、唤醒进程。
进程创建:
1.功能:创建一个具有唯一标识符的进程。创建进程需要的参数有唯一标识符、优先级、程序起始地址、CPU初始状态以及进程所需要的资源。
2.创建进程的过程:首先操作系统还会创建一个空白的PCB,然后获得并赋予进程唯一标识符ID,然后为进程分配空间,然后初始化PCB(例如将唯一标识符ID写入PCB),最后将该进程(PCB)插入相应的就绪队列。
进程撤销:
1.功能:撤销一个指定的进程,收回进程所占用的资源,撤销该进程的PCB。
2.进程撤销的时机:进程正常结束或进程异常结束或进程受到外界干预而不得不结束。撤销进程所需要的参数仅需要进程的唯一标识符。
3.进程撤销的过程:操作系统首先在PCB队列(进程队列)(存放PCB的数据结构)通过进程唯一标识符检索出指定的PCB,然后读取PCB的状态,如果该进程为运行态,就会改变其状态并使其终止,然后释放进程所占的资源,最后将PCB从PCB队列移除。需要注意的是,在Linux中,进程可能有一个或多个子进程,这些子进程也可能有一个或多个子进程......在撤销一个进程时,操作系统需要递归式的检查该进程是否有子进程,如果确实有,则先撤销子进程。
进程阻塞:
1.功能:停止进程的执行,使其成阻塞态。
2.阻塞的时机:进程请求操作系统完成某个服务,而由于某种原因,操作系统不能立即满足进程的要求;进程启动某种I/O操作,由于I/O操作非常缓慢,所以进程必须阻塞等待该操作完成;新数据尚未到达,例如准备接收一个信号,但该信号迟迟未到;无新工作可做,通常是进程的自我阻塞,例如程序员故意安排一个sleep()调用。
3.操作系统阻塞一个进程所需要的参数:在现代操作系统中,操作系统要控制进程的阻塞有必要获取进程阻塞的原因,因为不同的阻塞原因会构建不同的阻塞队列,不同的阻塞队列有不同的管理策略。这样能大大提高计算机的工作效率。
4.进程阻塞的过程:操作系统将当前正在运行的进程终止,将运行态改为阻塞态;根据阻塞原因插入到相应的阻塞队列;由调度程序完成进程发起的阻塞请求。
进程唤醒:
1.功能:唤醒处于阻塞队列当中的某个进程
2.唤醒进程的时机:阻塞队列当中的队头进程得到了想要的东西(例如系统完成了请求的服务、I/O操作完成、信号到达等),操作系统将这个进程从阻塞队列拿出,放入就绪队列。
原语:原语是由若干指令构成的具有特定功能的函数,这个函数在执行过程中不可被中断。在外部看来,原语具有原子性(只有未完成和完成两态)。
进程控制原语:
进程控制模块是操作系统的一部分,而操作系统在完成进程控制时不希望被任何东西打断,所以所有的进程控制模块都是原语:创建原语、撤销原语、阻塞原语、唤醒原语......
思考题:
1.为什么根据进程不同的阻塞原因构建不同的阻塞队列,能提高计算机的工作效率?
进程发起阻塞的原因是有多种的,每种请求所消耗的时间都是不一样的。如果都使用一个统一的阻塞队列,当一个进程发起一个系统服务请求时,会被放入这个统一的阻塞队列当中,假设改系统服务仅需2ms完成,而这个进程被放入阻塞队列之前,已经有一个正在等待I/O完成的进程已经在队列当中了,它需要等待40ms,这就会造成请求系统服务的进程多阻塞40ms,这是非常耽误工作效率的行为。
4.3线程
线程概念:
1.线程是CPU直接运行的实体,是CPU调度的基本单位。线程的概念引出之后,进程将不再是CPU调度的基本单位。
2.一个进程可以有多个执行路径,这些路径叫做线程。
3.多个线程可以同时共享CPU,从而实现并发。
什么是执行路径?在不使用多线程技术的编程当中,进程只有一条执行路径。假设我们有一个画圆函数和一个画方函数,则进程只能串行的去执行这两个函数。但是,当使用多线程技术时,可以分配多个执行路径,可以让线程1去执行画圆函数,线程2去执行画方函数,因为线程可以同时共享CPU,所以这两个函数是一起执行的,就达到了画圆的同时画方的目的。
单线程程序:整个进程只有一个线程,不适用多线程技术时都是单线程程序,这个线程称为主线程。
多线程程序:整个进程有至少两个线程,多个线程当中一定存在一个主线程。
多线程的典型应用场景:
1.程序的多个功能需要并发运行:例如在线视频程序,在线视频程序需要将视频解码、音频解码、网络接收等等,这几个模块我们不希望它们是串行执行的(因为这样会造成视频播放完了才播放音频),我们希望这几个模块是并发的。
2.具有窗口互动的程序:窗口是用户看到的前台,用户操作实际操作的就是前台,前台接收到的数据要发送给后台。这个过程我们也希望是并发执行的,在后台计算的过程当中用户还能够与前台发生交互,而不是后台在计算时,前台的功能就丧失了。
3.多核CPU上的程序:当CPU有多个核时,我们希望能够高效利用CPU的计算能力,就要使用多线程技术,占用CPU的多个核。
多线程的缺点:多线程并不是完美的,它也会带来许多让人头疼的问题。例如程序调试起来是非常困难的,因为是多个执行流并发执行的;并发的过程难以控制,因为CPU的调度是随机的,我们不能预测;线程安全问题,这也是最严重的问题,当多个线程同时访问同一份资源时,就会产生数据冲突、数据不一致等等问题。
思考题:
1.给定两个函数,Rotate的功能是光标旋转,Progress的功能是进度条。利用C++线程库编写一个程序使这两个函数可以并发执行。(利用了EasyX图形库插件)
void Rotate()//旋转
{
char buffer[4] = { '|','/','-','\\' };//注意转义字符
int index = 0;
while (true)
{
char puts[2] = { 0 };
puts[0] = buffer[index++];
outtextxy(10, 10, puts);
index %= 4;
Sleep(100);
}
}
void Progress()//进度条
{
while (true)
{
char buffer[102] = { 0 };
int cnt = 0;
while (cnt <= 100)
{
outtextxy(10, 40, buffer);
fflush(stdout);
buffer[cnt++] = '#';
Sleep(50);
}
cleardevice();
}
}
可以发现这两个函数都是死循环,就是说执行Rotate就不会执行Progress,执行Progress就不会执行Rotate。我们的最终方案如下:
void Rotate()//旋转
{
char buffer[4] = { '|','/','-','\\' };//注意转义字符
int index = 0;
while (true)
{
char puts[2] = { 0 };
puts[0] = buffer[index++];
outtextxy(10, 10, puts);
index %= 4;
Sleep(100);
}
}
void Progress()//进度条
{
while (true)
{
char buffer[102] = { 0 };
int cnt = 0;
while (cnt <= 100)
{
outtextxy(10, 40, buffer);
fflush(stdout);
buffer[cnt++] = '#';
Sleep(50);
}
cleardevice();
}
}
int main()
{
initgraph(1024, 480);
//主线程分出两个执行路径(创建两个线程,现在就有三个线程了)
thread t1(Rotate);//线程1执行Rotate
thread t2(Progress);//线程2执行Progress
t1.join();
t2.join();
return 0;
}
4.4临界资源与临界区
4.4.1临界资源与临界区
我们先看两段伪代码:
如果说,变量i是程序A和程序B的一个全局可见变量,并且程序A和程序B是并发运行,那么最后的输出结果可能是这样的:
造成结果1的原因可能是程序A先执行完,程序B再执行;造成结果2的原因可能是程序A执行到3)时中断,程序B执行完毕,此时i的值由100被程序B修改为了200;造成结果3的原因可能是程序B执行到3)时中断,程序A执行完毕,此时i的值由200被程序A修改为了100。
因为并发的执行过程是不可预见的,所以想要保证程序A和程序B每次执行都能百分之百输出正确结果时,就需要设定一个特定区域,这个特定区域不能让两个程序同时进入,只能先后进入。
我们让绿色部分为我们所谓的特定区域,那么程序A在执行这个区域的语句时,程序B必须在1)语句执行结束准备进入特定区域时阻塞;等程序A执行到5)语句时程序B才能够继续向下执行。通过这样的方法就能够确保程序A和程序B每次都输出正确的结果。
注意:以下所说的"执行路径"可能是进程,也可能是线程。
临界资源(Critical Resource):一次只允许一个执行路径独占访问、使用的资源。例如上述处于特定区域的i变量。
临界区(Critical Section):执行路径访问临界资源的程序段。例如上述的绿色部分的特定区域。
临界区和临界资源的访问特点:并发的执行路径不能同时进入临界区,也就是排他性(互斥)。
临界区访问机制的四个原则:
1.忙则等待:当临界区正在被一个执行路径占有时,这个临界区就处于忙状态,处于忙状态的临界区不能被其他执行路径占有。
2.空闲让进:当临界区没有执行路径占有时,这个临界区就处于空闲状态,处于空闲状态的临界区能且仅能被任意一个具有权限的执行路径占有。
3.有限等待:执行路径进入临界区的请求应该在有限时间内得到满足,否则就会让这个执行路径处于饥饿状态(也就是请求一个东西时迟迟得不到响应)。
4.让权等待:当某一执行路径在临界区外被阻塞时,它应当放弃对CPU的占有,从而让其他执行路径得到CPU。
思考题:
1.临界区设置的大些好,还是小些好?
我认为是尽量小。如果临界区设置的过大,就会失去并发的意义。
4.4.2锁机制
我们要把共享的资源(能被多个执行路径同时访问)变成临界资源,就需要一种保护机制,这个保护机制的过程就是将访问共享资源的程序段设置为临界区,设置为临界区的机制我们称为锁。
基本原理:
上锁操作:
1.设置一个"标志",假设它为S
2.这个S为1时,表明临界资源可用;为0时,表明临界资源不可用
3.若临界资源处于不可用状态,想要访问这个临界资源的执行路径应当在临界区外等待
4.若临界资源处于可用状态,那么执行路径可以进入临界区访问该临界资源,同时需要将S由1置0
开锁操作:
执行路径离开临界区时,需要将S由0置1
我们以一段伪代码来说明锁机制的原理:
上锁过程:当执行路径进入临界区时,先进行上锁操作,直接将S设为0并退出该函数,此时该执行路径可以进入临界区访问临界资源,同时这个临界资源处于不可用状态;当另一个执行路径要进入临界区时,首先做的工作也是上锁,但此时S的值为0,所以会执行goto语句一直跳转执行if语句,从而达到阻塞效果。
开锁过程:当正在处于临界区的执行路径离开临界区后,应当紧接着将S的值设为1,表明该临界资源可用。
我们再以流程图的方式重新对程序A和程序B"加工":
上面介绍的锁机制能够解决互斥问题,但同时我们需要注意锁的竞争问题:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
#include <unistd.h>
int tickets = 1000;
mutex mtx;
void start_routine(const char* name)
{
while(true)
{
mtx.lock();
if(tickets > 0)//如果票还够
{
cout << name << " get ticket:" << tickets-- << endl;
mtx.unlock();
}
else
{
mtx.unlock();
break;
}
}
}
int main()
{
thread t1(start_routine,"pthread 1");
thread t2(start_routine,"pthread 2");
t1.join();
t2.join();
return 0;
}
明显可以看出输出的结果并不是交替抢票。原因就在于锁机制确实能够确保对临界资源的互斥访问,当线程1开锁之后会立即执行循环进入下一次上锁,此时即使线程2已经在临界区外等候,但线程2对于锁的竞争能力没有线程1强(因为线程1距离锁最近)。
4.5同步和P-V操作
4.5.1同步和互斥的概念
互斥慨念:上述的锁机制就能解决互斥问题。互斥指的是在任意时刻,一份公共资源只能被一个执行流访问。
同步关系:若干执行路径为了完成一个共同的任务,需要相互协调运行步伐(上面的抢票程序中的两个线程就没有相互协调运行步伐);实现同步关系的关键在于一个执行路径开始某个操作之前必须要求另一个执行路径已经完成了某个操作,否则前者执行路径只能等待(也就是必须保证互斥)。
现实生活中同步关系的例子:司机与售票员:
1.司机要做的工作就是起步、行驶、停车,在起点站和终点站之间一直重复这三个动作
2.售票员要做的工作就是关门、售票、开门,在起点站和终点站之间一直重复这三个动作
3.司机和售票员之间就会产生一种微妙的同步关系:司机要起步,售票员就必须先关门;售票员要开门,司机就必须先停车。
4.5.2P-V操作概念
单纯使用锁机制不一定能够完成同步任务,我们可以使用信号量来完成这个任务。
红绿灯能够控制各向车流有序通过交叉路口,这也产生了一种同步关系:车要行驶经过交叉路口,红绿灯就必须是绿色的;如果红绿灯是红色的,车就必须等待。
这种红绿灯思想恰好用于操作系统中同步的基本思想:执行路径在运行过程中受信号量的状态控制,并能改变信号量;信号量的状态不同可以使执行路径阻塞还是唤醒;信号量的状态可以被执行路径修改。
信号量的P-V操作:
信号量的本质就是一个资源计数器,这个计数器描述了公共资源的数量。信号量其中还有一个阻塞队列。
1.P操作(荷兰语Passeren,通过的意思):执行路径在进入临界区之前,要先执行P操作;P操作的过程会将S值减1(表示该资源被使用一份),若差值大于或等于0,P操作函数退出,执行路径进入临界区;若差值小于0,该执行路径将会被放入阻塞队列当中。
2.V操作(荷兰语Vrijgeven,释放的意思):执行路径在离开临界区之后,首先执行V操作;V操作的过程会将S值加1(表示该资源被释放一份),若和大于0,V操作函数退出,执行路径继续向后执行(和大于0,表明阻塞队列没有执行路径);若和小于等于0,该执行路径会先从阻塞队列唤醒一个执行路径,再退出V操作函数,向后执行(和小于等于0,表明S加1之前S是负数,阻塞队列存在执行路径)。
4.5.3P-V操作解决互斥问题
前面提到过解决互斥问题的一种机制——锁机制。根据P-V操作的概念可以发现P-V操作也能解决互斥问题。
互斥问题的实质:实现对临界资源的互斥访问,即只允许一个执行路径进入临界区。
P-V操作解决互斥问题的过程:想要进入临界区的执行流必须先执行P操作,执行P操作的时候有可能会被阻塞;执行路径离开临界区后要执行V操作,执行V操作的时候有可能会唤醒一个执行路径。关键点在于信号量的S初值要设置合理。
以一个具体的例子来更好的理解P-V操作实现互斥:
假设有3个执行路径P1、P2、P3,每一个执行路径都有一个临界区CSa、CSb、CSc,这些临界区都对同一份临界资源访问。因为三个执行路径看到的临界资源只有一份,我们可以把信号量的S初值设为1。
可以得出结论,可以通过设置合理的信号量初值,配合信号量的机制就可以结局互斥问题。
思考题:
1.如上述的三个执行流Pa、Pb、Pc,这个三个执行流并发执行的过程中,是不是一定会发生阻塞和唤醒操作?如果将信号量的S初值设为0会有结果?设为2会有什么结果?应该如何合理设置信号量的初值?
它们三个在并发执行的过程中不一定会发生阻塞和唤醒操作,其原因在于前面我们讲过单道批处理系统,它在宏观上实现并发但微观上是一种串行,如果Pa、Pb、Pc三个执行流都是串行执行的,那么就不会发生阻塞和唤醒的情况(Pa执行完再到Pb执行......)。如果将信号量的处置设为0表示没有临界资源,所以任何一个执行流都不可能进入临界区;如果将信号量的初值设为2则表示有两份临界资源,但实际上只有一份,就会导致有两个执行流同时进入临界区同时访问临界资源。所以,要想合理的设置信号量的初始值,应该以共享资源的数量来判断。
4.5.4P-V操作解决同步问题
同步机制的实质:运行条件不满足时,执行路径应当暂停执行(阻塞);运行条件满足时,能让执行路径立即执行。这两个点互相配合就能实现多个执行路径互相协调的向后执行。
P-V操作解决同步问题的基本思路:
1.阻塞当前执行路径:在进行关键操作(通常指进入临界区)之前执行P操作,必要时可以阻塞
2.继续执行:在关键操作之后(通常指离开临界区之后),执行V操作,必要时可以唤醒某个正在阻塞的执行路径
3.必须定义有意义的信号量,并设置合适的初值
再次分析司机与售票员的例子:
1.售票员关门,司机才可以起步
2.司机停车,售票员才可以开门
3.仔细分析之后可以发现要设置两个信号量,一个表示车门的状态,一个表示车辆运行的状态(起步与停车)
那么我们可以以伪代码的形式来解决这个简单的同步问题:
这里我们需要注意一个点,信号量的计数器不仅仅可以表示资源的数量,也可以表示资源使用的状态。
思考题:
1.信号量即可以表示公共资源的数量,又可以表示资源使用的状态,我们应该如何合理设定信号量的初值?
如果多个执行流需要访问同一份资源,这时我们就需要先保证互斥,以资源的数量来设定初值;如果多个执行流访问不同的资源,但每个执行流访问自己的那份资源时需要知道其他执行流资源的状态,就应该以资源使用的状态来设定信号量初始。上述司机与售票员的例子,司机只负责开车和停车,售票员只负责开门和关门,他们没有共同访问同一份资源,但是司机要想起步,就必须确保售票员已经把门关好,售票员想开门,就必须确保司机已经停车。
4.5.5经典互斥与同步问题
1.生产者消费者问题:一群生产者(Productor)向一群消费者(Consumer)提供产品(数据),他们并不是直接交互的,而是通过一块特定的缓冲区。
我们明确以下几点规则:
1.生产者不能向满缓冲区存产品
2.消费者不能从空缓冲区取产品
3.缓冲区在任意时刻只能被一个生产者或消费者存或取
具体分析规则可以得到:生产者与生产者之间要有互斥关系(不能同时向缓冲区存产品);消费者与消费者之间要有互斥关系(不能同时从缓冲区拿产品);生产者与消费者之间既要有互斥关系又要有同步关系(只生产不消费是不合理的;只消费不生产也是不合理的)。
我们以伪代码来实现这两者之间的关系:
如果真正把上面的代码实现,那么生产者生产一个产品,消费者就会消费一个产品;如果缓冲区为空消费者会阻塞(不会消费);如果缓冲区为满生产者会阻塞(不会生产)。具体的代码实现将在后期Linux环境编程中提及。
2.读者和写者问题:有一本书,有一个或多个读者读;有一个或多个写者写。
我们明确以下几点规则:
1.允许多个读者同时读
2.只允许一个写者写
3.不允许读者和写者同时读和写
我们以伪代码实现这两者的关系:
像上面这样确实能够保证写者与写者之间、读者与写者之间的互斥,但这样做有些"一刀切"的味道。我们需要注意,读者与读者之间不存在互斥!我们可以添加一个读者计数变量(readCount),每次读者执行流要读书时,变量加1;仔细思考可以发现,只要有一个读者,那么写者就不能写书,所以我们要做到的便是只要第一个读者进来读书,我们就利用P-V操作与写者互斥,此时无论读者的数量为多少,都能保证与写者的互斥;反之,当最后一个读者选择不读书时,读者计数变量减1;直到最后一个读者退出,此时才能让写者写书。那么改进之后的设计如下:
此时还存在一个问题!readCount是读者的全局可见变量,也就是多个读者执行流共享的变量,那么它也要被当成临界资源被保护!所以我们还需要做最后一步工作: