操作系统——进程管理

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是读者的全局可见变量,也就是多个读者执行流共享的变量,那么它也要被当成临界资源被保护!所以我们还需要做最后一步工作:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/17657.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【分布式技术专题】「授权认证体系」OAuth2.0协议的入门到精通系列之授权码模式

这里写目录标题 OAuth2.0是什么OAuth2.0协议体系的Roles角色OAuth定义了四个角色资源所有者资源服务器客户端授权服务器 传统的客户机-服务器身份验证模型的问题 协议流程认证授权授权码 OAuth2.0是什么 OAuth 2.0是用于授权的行业标准协议。OAuth 2.0专注于简化客户端开发人员…

一文介绍Linux EAS

能量感知调度&#xff08;Energy Aware Scheduling&#xff0c;简称EAS&#xff09;是目前Android手机中Linux线程调度器的基础功能&#xff0c;它使调度器能预测其决策对CPU能耗的影响。依靠CPU的能量模型&#xff08;Energy Model&#xff0c;简称EM&#xff09;&#xff0c;…

疑难问题定位案例复盘(三)

今天我们分享一个数据库被异常改写的案例&#xff0c;通过该案例我们可以学习总结出常规的文件被改写问题定位思路。 问题现象 1、测试环境在进行特定压力测试时发现页面登陆异常&#xff0c;且调试日志多个进程持续打印“数据库打开失败”日志。 2、测试环境在进行多个压力测…

【机器学习】决策树(实战)

决策树&#xff08;实战&#xff09; 目录 一、准备工作&#xff08;设置 jupyter notebook 中的字体大小样式等&#xff09;二、树模型的可视化展示1、通过鸢尾花数据集构建一个决策树模型2、对决策树进行可视化展示的具体步骤3、概率估计 三、决策边界展示四、决策树的正则化…

PyCharm2023.1下载、安装、注册以及简单使用【全过程讲解】

在使用PyCharm IDE之前&#xff0c;请确保自己的计算机里面安装了Python解释器环境&#xff0c;若没有下载和安装可以看看我之前的文章>>>Python环境设置>>>或者还可以观看视频讲解。 注意&#xff1a;本文软件的配置方式仅供个人学习使用&#xff0c;如有侵…

02- 目标检测基础知识及优化思路汇总 (目标检测)

要点&#xff1a; 参考综述&#xff1a;深度学习目标检测最全综述 - 爱码网参考表达&#xff1a;https://www.cnblogs.com/xjxy/p/13588772.html 一 发展历程 分类网络是目标检测的基础&#xff0c;必须熟练掌握。 1.1 传统算法 V.J Detector 19年前&#xff0c;P. Viola 和 …

【java】Java 异常处理的十个建议

文章目录 前言一、尽量不要使用e.printStackTrace(),而是使用log打印。二、catch了异常&#xff0c;但是没有打印出具体的exception&#xff0c;无法更好定位问题三、不要用一个Exception捕捉所有可能的异常四、记得使用finally关闭流资源或者直接使用try-with-resource五、捕获…

全注解下的SpringIoc 续4-条件装配bean

Spring Boot默认启动时会加载bean&#xff0c;如果加载失败&#xff0c;则应用就会启动失败。但是部分场景下&#xff0c;我们希望某个bean只有满足一定的条件下&#xff0c;才允许Spring Boot加载&#xff0c;所以&#xff0c;这里就需要使用Conditional注解来协助我们达到这样…

Java面试题总结 | Java面试题总结10- Feign和设计模式模块(持续更新)

文章目录 Feign项目中如何进行通信Feign原理简述 设计模式spring用到的设计模式项目的场景中运用了哪些设计模式写单例的时候需要注意什么工厂模式的理解设计模式了解么工厂设计模式单例设计模式代理设计模式策略模式**模板方法模式**观察者模式**适配器模式**观察者模式**适配…

HNU-操作系统OS-实验Lab2

OS_Lab2_Experimental report 湖南大学信息科学与工程学院 计科 210X wolf &#xff08;学号 202108010XXX&#xff09; 前言 实验一过后大家做出来了一个可以启动的系统&#xff0c;实验二主要涉及操作系统的物理内存管理。操作系统为了使用内存&#xff0c;还需高效地管理…

【算法与数据结构】顺序表

顺序表 数据结构 结构定义结构操作 顺序表&#xff1a;结构定义 一个数组&#xff0c;添加额外的几个属性&#xff1a;size, count等 size: 数组有多大 count: 数组中当前存储了多少元素 顺序表三部分&#xff1a; 一段连续的存储区&#xff1a;顺序表存储元素的地方整型…

利用css实现视差滚动和抖动效果

背景&#xff1a; 前端的设计效果&#xff0c;越来越炫酷&#xff0c;而这些炫酷的效果&#xff0c;利用css3的动画效果和js就可以实现&#xff0c;简单的代码就能实现非常炫酷的效果。 原理&#xff1a; 利用 js监控scrollTop的位置&#xff0c;通过 top定位图片的位置&#x…

HDOJ 1022 Train Problem Ⅰ 模拟栈操作

&#x1f351; OJ专栏 &#x1f351; HDOJ 1022 Train Problem Ⅰ 输入 3 123 321 3 123 312输出 Yes. in in in out out out FINISH No. FINISH&#x1f351; 思路 &#x1f364; 栈顶元素与目标元素不匹配就进栈&#xff0c;匹配就出栈 &#x1f364; 匹配完&#xff1a;y…

『python爬虫』10. 数据解析之xpath解析(保姆级图文)

目录 安装库xpath入门怎么快速得到xpath路径xpath节点的关系xpath方法小型实战总结 欢迎关注 『python爬虫』 专栏&#xff0c;持续更新中 欢迎关注 『python爬虫』 专栏&#xff0c;持续更新中 安装库 pip install lxmlxpath入门 怎么快速得到xpath路径 &#xff08;相对路…

webpack plugin原理以及自定义plugin

通过插件我们可以拓展webpack&#xff0c;加入自定义的构建行为&#xff0c;使webpack可以执行更广泛的任务。 plugin工作原理&#xff1a; webpack工作就像是生产流水线&#xff0c;要通过一系列处理流程后才能将源文件转为输出结果&#xff0c;在不同阶段做不同的事&#x…

二、PEMFC基础之电化学与反应动力学

二、PEMFC基础之电化学与反应动力学 1.电流、电流密度2.反应速率常数3.交换电流密度4.电化学动力学奠基石B-V方程5.活化损失计算Tafel公式6.计算案例 1.电流、电流密度 由法拉第定律 i d Q d t n F d N d t i\frac{dQ}{dt}\frac{nFdN}{dt} idtdQ​dtnFdN​ j i A j\frac{…

【五一创作】ERP实施-委外业务-委外采购业务

委外业务主要有两种业务形态&#xff1a;委外采购和工序外协&#xff0c;委外采购主要是在MM模块中实现&#xff0c;工序外协主要由PP模块实现&#xff0c;工序外协中的采购订单创建和采购收货由MM模块实现。 委外采购概念 委外采购&#xff0c;有些企业也称为带料委外或者分包…

牛客刷SQL题Day5

SQL69 返回产品并且按照价格排序 select prod_name , prod_price from Products where prod_price between 3 and 6 select prod_name , prod_price from Products where 6>prod_price and prod_price >3 踩坑1&#xff1a; between......and.......包括边界。 踩坑2&am…

网卡丢失导致集群异常

假期晚上有个电话&#xff0c;说集群故障&#xff0c;应用无法连接&#xff0c;节点一可以ssh登录&#xff0c;节点二已无法正常登录了&#xff0c;在节点一上需要ssh 私网ip地址才可以登录节点二&#xff0c;虽不是重点客户&#xff0c;有问题还是需要积极处理。 首先看集群状…

Cartesi 2023 年 4 月回顾

查看你不想错过的更新 2023年5月1日&#xff0c;感谢Cartesi生态系统中所有了不起的构建者&#xff01; 在一个激动人心的旅程之后&#xff0c;我们的首届全球线上黑客马拉松正式结束了&#xff01;有超过200名注册建造者参加&#xff0c;见证了所有参与者展示的巨大才华和奉献…