更多优质内容
请关注公众号

操作系统入门(八)进程互斥同步经典模型——生产者消费者问题、多生产者消费者问题和读者写者问题-张柏沛IT博客

正文内容

操作系统入门(八)进程互斥同步经典模型——生产者消费者问题、多生产者消费者问题和读者写者问题

栏目:其他内容 系列:操作系统入门 发布时间:2022-02-14 11:02 浏览量:1652
本系列文章目录
展开/收起

生产者消费者模型

系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者 进程每次从缓冲区中取出一个产品并使用。(注:这里的“产品”理解为某种数据)

生产者、消费者共享一个初始为空、大小为n的缓冲区。

只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待。(同步关系)

只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。(同步关系)

缓冲区是临界资源,各进程必须互斥地访问。(互斥关系)

请用代码实现上述模型。

题目中存在2个同步关系和1个互斥关系,因此需要3个信号量实现:

semaphore mutex = 1; //互斥信号量,实现对缓冲区的互斥访问
semaphore empty = n; //同步信号量,表示空闲缓冲单位的数量
semaphore full = 0; //同步信号量,表示产品的数量,也即非空缓冲单位的数量

producer(){
    while(1){
        生产1个产品;
        P(empty);    // 消耗一个空闲缓冲单位
        P(mutex);	  // 访问缓冲区
        把产品放入缓冲区;
        V(mutex);
        V(full);	// 增加一个产品
    }


consumer(){
    while(1){
        P(full);    // 消耗一个缓存区中的产品
        P(mutex);	  // 访问缓冲区
        从缓冲区取出产品;
        V(mutex);
        V(empty);	// 增加一个可用缓冲区单位
        使用产品;
    }
}

 

问题1:producer()中能否改变 P(empty) 和 P(mutex)的顺序?

答:不可以,会发生死锁。

若此时缓冲区内已经放满产品,则 empty=0,full=n。

则生产者进程执行① 使mutex变为0,再执行②,由于已没有空闲缓冲区,因此生产者被阻塞。

由于生产者阻塞,因此切换回消费者进程。消费者进程执行③,由于mutex为0,即生产者还没 释放对临界资源的“锁”,因此消费者也被阻塞。

这就造成了生产者等待消费者消费以腾出空闲单元,而消费者又等待生产者释放临界区,生 产者和消费者循环等待被对方唤醒,出现“死锁”。

因此,实现互斥的P(wait)操作一定要在实现同步的P(wait)操作之后

 

问题2:producer()中能否改变V(full) 和 V(mutex)的顺序?

答:V操作不会导致进程阻塞,因此两个V操作顺序可以交换。

 

问题3:能否把 “使用产品;” 放到PV操作之间?

答:可以是可以,但是会使临界区代码增多,临界区是只允许串行的代码区,因此会导致并发度降低,建议极可能将无需互斥访问的代码放到临界区之外。

 

 

多生产者-多消费者模型

桌子上有一只盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放 橘子,儿子专等着吃盘子中的橘子,女儿专等着吃盘子中的苹果。只有盘子空时,爸爸或妈妈才 可向盘子中放一个水果。仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。

用PV操作实现上述过程。

1、首先4个人,是4个进程;

2、父母只能在盘子有空间的时候放水果,否则阻塞。需要同步信号量plate表示盘子是否为空来同步父母的放水果操作,对应的V(plate)操作放在子女进程中取完水果后执行,唤醒父母。

3、子女需要两个同步信号量apple和orange表示是否有苹果和橘子来同步子女的拿水果操作。对应的V操作在父母放完水果后执行,唤醒子女。

4、互斥访问盘子资源需要一对PV操作。

 

代码如下:

orange = 0;	// 盘子中的橘子数	
apple = 0;		// 盘子中的苹果数
empty = 1;		// 盘子是否为空
mutex = 1;

father(){
    while(1){
        生产苹果;
        P(empty);	// 如果盘子不为空则阻塞,为空则将empty置为非空
        P(mutex);
        苹果放入盘子;
        V(mutex);
        V(apple);	// 通知女儿有苹果了
    }
}

mother(){
    while(1){
        生产橘子;
        P(empty);
        P(mutex);
        橘子放入盘子;
        V(mutex);
        V(orange);
    }
}

son(){
    while(1){
        P(orange)	// 检查盘子中是否有橘子,并减少盘子中的一个橘子。没有则阻塞
        P(mutex);
        从盘子拿走橘子;
        V(mutex);
        V(empty);
    }
}

daughter(){
    while(1){
        P(apple)
        P(mutex);
        从盘子拿走苹果;
        V(mutex);
        V(empty);		// 通知爸爸盘子空了,可以继续放苹果
    }
}

问题1:可不可以不用互斥信号量mutex?

答:在本例中可以,因为缓冲区大小为1,在任何时刻,apple、orange和empty这三个同步信号量最多只有一个是1,因此任意时刻最多只有一个进程的P操作不会被阻塞,其他进程都会被阻塞,也就相当于是串行操作了。

如果缓冲区大小为2,则必须要用互斥信号量,否则父母可能同时访问临界资源导致数据混乱。

 

 

读者-写者问题

情景如下:

有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致 数据不一致的错误。

因此要求:①读读可并发;②写写互斥;③读写互斥;④写读互斥。

读者-写者问题和生产者-消费者问题的区别在于,前者的读读是可以并发的,读是不会改变容器内的数据的,后者的消费者不能并发消费容器内的产品,因为消费会修改容器内的产品(即产品-1)。

 

分析:本题目是一个纯互斥的问题,不涉及同步(因为没有必须先读后写,或者先写后读的要求)。

其实 后面3个要求比较容易解决,直接用一个互斥量锁住读操作和写操作的临界区即可,如何解决读读并发是难点。

读者-写者问题可以使用一个互斥信号量,一个计数器实现。

可以用一个计数器count记录当前执行读操作的读进程有多少个,只有第一个读进程才需要读前加锁,count > 0 时,说明有其他读进程在读,本读进程就不上锁了。

这就相当于,所有读进程可以共同持有一把锁,而写进程不能共享一把锁,只能单独使用这把锁

 

代码实现:

rw_lock = 1;	// 互斥量,保证读写数据是串行的
count = 0; 	// 当前执行读操作的读进程有多少个
count_lock = 1;		// 互斥量,保证count变量的访问是串行的

reader(){
    while(1){
        P(count_lock)
        if(count==0){
            P(rw_lock)    // 没有读进程上锁,则本读进程上锁
        }
        count++
        V(count_lock)
        
        读数据;
        
        P(count_lock)
        count--
        if(count==0){
            V(rw_lock)    // 如果本进程是最后一个持有锁的读进程,则解锁
        }
        V(count_lock) 
    }
}

writer(){
    while(1){
        P(rw_lock);	// 申请锁,上锁
        写数据;
        V(rw_lock);	// 释放锁,解锁
    }
}

需要注意:count的检查和修改是原子性的。否则,读进程1执行完 if(count==0)后对文件上锁,没来得及执行 count++就切换到了读进程2,那么读进程2也会上锁,于是读读互斥。

 

解决写饥饿问题

问题:按照上述代码的逻辑,如果有足够多的读进程,他们执行while循环不停地进行读操作,那么 rw_lock 会一直被读进程们持有,导致写进程一直等待,无法写入数据。如何解决写进程饥饿问题?

比如,有3个进程并发执行,开始执行的顺序是读进程1->写进程1->读进程2。之所以写进程1会饥饿是因为写进程1被P(rw_lock)阻塞住了,读进程1在执行count--之前,读进程2就执行了count++,导致读进程1无法释放 rw_lock。

如果能按进程先来后到的顺序公平的获得rw_lock就可以破解饥饿,例如写进程1比读进程2先来,写进程1就优先获得rw_lock的权利,因此写进程1可以先预订读进程1正在持有的rw_lock,即用一个互斥量w锁住rw_lock,只有进程拿到rw_lock锁之后才能释放w。可以将 P(w)理解为进程在申请获取rw_lock锁的权利。

如下所示:

rw_lock = 1;	// 互斥量,保证读写数据是串行的
count = 0; 	// 当前执行读操作的读进程有多少个
count_lock = 1;		// 互斥量,保证count变量的访问是串行的
w = 1;    // 互斥量,用来锁住rw_lock

reader(){
    while(1){
        P(w);   // 申请持有rw_lock锁的权利
        P(count_lock)
        if(count==0){
            P(rw_lock)    // 没有读进程上锁,则本读进程上锁
        }
        
        count++
        V(w);    // 拿到rw_lock锁之后才释放w,必须放在count++之后,因为对于读进程而言,count++才算让读进程拿到了rw_lock锁
        V(count_lock)
        
        读数据;
        
        P(count_lock)
        count--
        if(count==0){
            V(rw_lock)    // 如果本进程是最后一个持有锁的读进程,则解锁
        }
        V(count_lock) 
    }
}

writer(){
    while(1){
        P(w);       // 申请持有rw_lock锁的权利
        P(rw_lock);	// 申请锁,上锁
        V(w);       // 拿到rw_lock锁之后才释放w
        写数据;
        V(rw_lock);	// 释放锁,解锁
    }
}

 

下图为官方答案

 

 




更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > 操作系统入门(八)进程互斥同步经典模型——生产者消费者问题、多生产者消费者问题和读者写者问题

热门推荐
推荐新闻