• 操作系统笔记------进程同步(3)


    进程同步问题

    生产者消费者问题

    描述

    这是一个多进程并发协作的问题(也可以是多线程),在这里我们涉及到了两类进程,一类是生产者进程,一类是消费者进程,生产者用于生产数据,消费者用于处理生产者生产的数据,所有消费者处理的数据必须由生产者生产得到。
    为了解耦这两类进程之间的关系,我们引入一个数据缓冲池,这个缓冲池用于存储数据。生产者将数据产生后投入这个缓冲池中,消费者直接由这个缓冲池中取出数据,这样就不必考虑这两个进程之间的相关依赖,即(解耦)。两个进程只需对缓冲区进行操作即可。生产者只有在缓冲区没有填满时才能继续生产,否则就会被阻塞,消费者只有在缓冲区中还有数据时才能对里面的数据进行操作,如果缓冲区为空,那么将没有数据供消费者处理,生产者进程会被阻塞。而且两个进程对缓冲区的访问可能是阻塞的(即缓冲池是临界资源)。

    图中P是生产者,C是消费者。
    先整理一下语义:缓冲不为空时消费者可以去数据处理,缓冲没有满时,生产者可以生产数据存入缓冲中。对缓冲的操作是阻塞访问。

    int full=0;//表示消费者可用的空间
    int empty=n;//表示生产者可用的空间
    //初始状态缓冲区全为空
    int visit=1;//对缓冲的访问
    //---------------上面全是信号量----------------
    Resuouse buf[n];//缓冲的空间
    int in=0;//生产者对buf的访问指针
    int out=0;//消费者对buf的访问指针
    //初始状态都为0
    

    接下来就是对于原语的一个伪代码描述

    wait(int &s)
    {
    	s--;
    	if(s<0)//S<0表示没有可以的资源则将其对应当前进程进行阻塞等待
    	{
    		block();
    	}
    }
    
    signal(int &s)
    {
    	s++;
    	if(s<=0)//释放使用的资源之后检查是否还有进程在申请,如果有就从队列中唤醒。
    	{
    		wakeup(list);//list为被阻塞的进程队列
    	}
    }
    

    由于这里的信号量所描述的都是大于零时可以访问,所以其原语对应操作相同。
    接下来就是生产者与消费者的伪代码描述

    Producer()
    {
    	while(true)
    	{
    		wait(empty);//保证缓冲区有空余空间
    		wait(visit);//保证对缓冲资源的阻塞式访问
    		buf[in]=data;//生产出相应数据
    		in=(in+1)%n;//这里的buf可以看成环状的结构
    		signal(visit);
    		signal(full);//设置缓冲区多一个处理好的数据
    	}
    }
    
    Consumer()
    {
    	while(true)
    	{
    		wait(full);//保证缓冲区有处理好的数据
    		wait(visit);//保证对缓冲资源的阻塞式访问
    		deal(buf[out]);//处理生产出的数据
    		out=(out+1)%n;//这里的buf可以看成环状的结构
    		signal(visit);
    		signal(empty);//设置缓冲区多一个空闲的区域
    	}
    }
    

    分情况讨论

    接下来我们来对不同情况进行分别讨论。
    主要是一下几种:
    单生产者单消费者、多缓冲时(缓冲区空间大于1)
    多生产者多消费者、单缓冲时(缓冲区空间为1)
    单生产者单消费者、单缓冲时(缓冲区空间为1)
    当缓冲区无限大(缓冲区空间为无穷大)

    上面的实现是一个标准的实现,可以适用于所有的情况。
    但是对于单缓冲的情况,我们可以将visit的信号量去除,因为只有一个缓冲空间,要么full为1,要么empty为1,所以不需要再使用一个visit信号量。
    对于空间足够大的缓冲区时,生产者不需要检查是否有空闲空间,所以不需要wait(empty),同时消费者处理完后也不用释放empty的空间,或者说不需要记录empty的信号量,所以可以删除对于empty的维护。

    信号量的释放顺序

    wait请求的顺序

    对于我们上面的代码实现,我们主要来讨论一下几个信号量的释放顺序。
    生产者的wait信号顺序:我们的实现是先申请empty再申请对于缓存区的访问,这个顺序如果颠倒有:

    //生产者
    wait( visit ) ;
    wait( empty ) ;
    .....
    signal( visit ) ;
    signal( full ) ;
    

    这样的颠倒会导致死锁的危险,如果没有空闲的缓存区,生产者获得了对缓存区的访问权,但是无法生产,wait(empty)阻塞,而消费者wait(full)成功,但是无法访问缓存区,这时生产者等待消费者处理资源后产生新的empty,消费者等待生产者释放对于缓存区的访问,产生死锁。

    同理当我们调换对于消费者的wait顺序时也有相同的隐患。

    //消费者
    wait( visit ) ;
    wait( full ) ;
    .......
    signal( visit ) ;
    signal( empty ) ;
    

    如果缓存区为空的时候,消费者获取了对于缓存区的访问,但是由于wait(full)被阻塞,(没有生产好的数据了),这时生产者进行生产,首先获取了wait(empty),但是没有对于缓存区的访问权了(wait(visit)阻塞),生产者等着消费者释放对于缓存的访问,消费者等着生产者生产数据。产生了死锁的隐患。

    signal请求的顺序

    对于请求的释放顺序就没有那么多的顾虑了,因为其不存在对于资源的占用,所以释放时的先后并无影响,即使存在依赖关系,在下一个时间片中相应的资源也会被释放,所以释放的顺序并无影响。

    读者写者问题

    有两类的进程,一类是读者进程,读者进程之间可以同时访问资源,这种访问是非阻塞的,还有一类进程是写者进程,写者进程对于资源的访问是阻塞式的,写者进程与读者进程之间的访问也是阻塞式的。也就是单个写者进程在写时不允许其他的进程对资源进行访问。这里的资源可以理解为文件。读写进程之间并无顺序。

    首先还是信号量的设置。

    int visit=1;//表示对于文件资源的访问
    //--------以上是信号量设置----------
    int rN=0;
    //由于读进程之间是可以同时对资源进行访问的,所以使用一个int来对其访问的进程数量进行记录。
    

    接下来是相关PV原语的实现(伪代码描述)。

    wait(int &s)
    {
    	s--;
    	if(s<0)//S<0表示没有可以的资源则将其对应当前进程进行阻塞等待
    	{
    		block();
    	}
    }
    
    signal(int &s)
    {
    	s++;
    	if(s<=0)//释放使用的资源之后检查是否还有进程在申请,如果有就从队列中唤醒。
    	{
    		wakeup(list);//list为被阻塞的进程队列
    	}
    }
    

    之后就是相关进程的实现了。

    //写进程
    Writer()
    {
    	while(true)
    	{
    		wait(visit);//保证对缓冲资源的阻塞式访问
    		write();//具体的写操作
    		signal(visit);
    	}
    }
    //读进程
    Reader()
    {
    	while(true)
    	{
    		if(rN==0) wait(visit);//如果rN不为0,表示已经有相关的读进程对资源进行访问,读进程之间不存在互斥访问,所以不用请求访问锁。
    		rN++;
    		read();//具体的读操作
    		rN--;
    		if(rN==0) signal(visit);//若读操作结束后没有读进程对资源访问,rN=0,所以这时释放对资源的访问。
    	}
    }
    

    虽然上面实现了基本的读写进程的并行处理,但是显然在读进程的操作中存在一个错误,就是rN的操作,这里的rN显然是一个临界资源,我们对其访问的时候应该注意互斥访问。
    所以正确的实现如下:

    //读进程
    int readNSign=1;//增加一个对读进程计数的信号量,保证对其互斥访问。
    Reader()
    {
    	while(true)
    	{
    		wait(readNSign) ;
    		if(rN==0) wait(visit);//如果rN不为0,表示已经有相关的读进程对资源进行访问,读进程之间不存在互斥访问,所以不用请求访问锁。
    		rN++;
    		signal(readNSign) ;
    		read();//具体的读操作
    		wait(readNSign) ;
    		rN--;
    		if(rN==0) signal(visit);//若读操作结束后没有读进程对资源访问,rN=0,所以这时释放对资源的访问。
    		signal(readNSign) ;
    	}
    }
    

    这样就实现了标准的进程并行处理。

    哲学家就餐问题

    5位哲学家围绕圆桌而坐,反复思考和进餐。但是只有5只碗和筷子,放置如图所示,只有当哲学家同时拿起碗边的2只筷子时,才能进餐。请用记录型信号量进行同步。

    这里的互斥信号量也就是桌上的筷子,抽象一下就是每个线程需要两个资源才能运作,但是资源的数量有限,如果同时请求只能满足每个进程一个资源的访问需求。但是每个进程需要两个资源才能进行工作,所以发生死锁的概率极大。
    要解决这一问题需要一个进程打破这个请求循环,也就是至少满足其中一个进程的请求,这样当这个进程完成运行后,释放相关的资源后,其他的进程便可以依次的请求到资源完成工作。
    下面是相关的伪代码描述:

    //信号量描述
    int sign[5]={1,1,1,1,1};//表示五个资源都是可以访问的。
    

    相关的原子操作与上面的操作一致
    接下来是相关的进程实现,我们用第五个哲学家来打破进程死锁,那么对于其他四个哲学家有:

    Philosopher(int i)
    {
    	while(true)
    	{
    		wait(sign[i]);
    		wait(sign[(i+1)%5]); 
    		eat(); 
    		signal(sign[i]);
    		signal(sign[(i+1)%5]);
    	}
    }
    

    最后一个哲学家有:

    Philosopher(int i)
    {
    	while(true)
    	{
    		wait(sign[(i+1)%5]); 
    		wait(sign[i]);
    		eat(); 
    		signal(sign[(i+1)%5]);
    		signal(sign[i]);
    	}
    }
    

    相当于所有的人都先去抢他们右手的筷子,左手的筷子被左边的人作为右手的筷子抢去,但是最后一人去抢的是左手的筷子,也就是和左边的人抢筷子,右边的筷子空缺,没有人抢,所以只要他抢到左边的筷子,两个筷子的资源就满足了,就可以进行工作了。
    其实这个问题的根源还是要一次性的获得所有的资源,不然进程无法工作,所以也可以试试AND类型的信号量,对于进程资源进行一次性的分配,这样一次请求之后进程就可以进行工作了。

    小结

    对于进程同步问题的处理关键还是在临界资源的设置与相关信号量的分配处理与释放处理上。有几个临界资源,每个临界资源什么时候释放,什么时候请求,以及相关的逻辑处理,原语的实现等等都应围绕临界资源与其信号量展开。

  • 相关阅读:
    Github 上热门的 Spring Boot 项目实战推荐
    深入理解建造者模式 ——组装复杂的实例
    别死写代码,这 25 条比涨工资都重要
    Spring Boot 使用 JWT 进行身份和权限验证
    秋招打怪升级之路:十面阿里,终获offer!
    一问带你区分清楚Authentication,Authorization以及Cookie、Session、Token
    适合新手入门Spring Security With JWT的Demo
    面试官:“谈谈Spring中都用到了那些设计模式?”。
    春夏秋冬又一春之Redis持久化
    Mysql锁机制简单了解一下
  • 原文地址:https://www.cnblogs.com/yanzs/p/13788258.html
Copyright © 2020-2023  润新知