进程管理3--经典的进程同步问题
本文地址:http://www.cnblogs.com/archimedes/p/classic-process-synchronization-problems.html,转载请注明源地址。
管程机制
用信号量机制实现进程间的同步和互斥,既方便又有效。但存在以下两个问题:
-
每个访问临界资源的进程都必须自备同步操作(P、V操作),这使大量的同步操作分散在各个进程中,给系统的管理带来麻烦。
-
会因同步操作使用不当而导致系统死锁。
解决方法:
管程(Monitors)
Dijkstra于1971年提出:为每个共享资源设立一个“秘书”来管理对它的访问。 一切来访者都要通过秘书,而秘书每次仅允许一个来访者(进程)来访问共享资源。这样既便于系统管理共享资源,又能保证进程的互斥访问和同步。1973年,Hanson和Hoare又把“秘书”概念发展为管程概念。
基本思想
系统中的各种软硬件资源,其特性都可用数据结构抽象地描述,把对该共享数据结构实施的操作定义为一组过程。
进程对共享资源的访问都是通过这组过程对共享数据结构的操作来实现的,这组过程可以根据资源的使用情况,接受或阻塞进程的访问,确保每次只有一个进程使用共享资源,
实现进程的互斥。
管程的定义
一个数据结构和在该数据结构上能被并发进程所执行的一组操作,这组操作能同步进程和改变管程中的数据。如下图所示:
管程的结构
TYPE <管程名> = MONITOR
<共享变量说明>;
procedure<过程名>(<形式参数表>);
begin
<过程体>;
end;
……
procedure <过程名>(<形式参数表>);
begin
<过程体>;
end;
begin
<管程的局部数据初始化语句序列>;
end;
管程相当于围墙,它把共享变量和对它进行操作的若干过程围了起来,所有进程要访问临界资源时,都必须经过管程才能进入,而管程每次只允许一个进程进入管程,从而实现进程互斥。
管程的特性
模块化:是一个基本程序单位,可单独编译
抽象数据类型:管程中不仅有数据,而且有对数据的操作
信息隐蔽:数据结构和过程的具体实现外部不可见
管程与进程作比较
管程定义的是公用数据结构,而进程定义的是私有数据结构PCB;
管程把共享变量上的同步操作集中起来,而临界区却分散在每个进程中;
管程是为管理共享资源而建立的,进程主要是为占有系统资源和实现系统并发性而引入的;
管程是被要使用共享资源的进程所调用的,管程和调用它的进程不能并发工作,而进程之间能并发工作,并发性是进程的固有特性;
管程是OS中一个资源管理模块,供进程调用;而进程有生命周期,由创建而产生、由撤销而消亡
经典进程的同步问题
在多道程序环境下,进程同步问题十分重要,出现一系列经典的进程同步问题,其中有代表性有:
-
生产者—消费者问题
-
哲学家进餐问题
-
读者—写者问题
一、“生产者—消费者”问题
问题描述:
“生产者---消费者”问题是最著名的进程同步问题。它描述了一组生产者向一组消费者提供产品,它们共享一个缓冲池(有n个缓冲区),生产者向其中投放产品,消费者从中取得产品。
它是许多相互合作进程的抽象,如输入进程与计算进程;计算进程与打印进程等。
定义两个同步信号量:
empty——表示缓冲区是否为空,初值为1。
full——表示缓冲区中是否为满,初值为0。
//生产者进程: while(TRUE){ 生产一个产品; P(empty); 把产品送往Buffer; V(full); } //消费者进程: while(TRUE){ P(full); 从Buffer取出一个产品; V(empty); 消费产品; }
扩展:M个生产者,K个消费者,公用有n个缓冲区的缓冲池
设缓冲池的长度为n(n>0),一群生产者进程P1,P2,…,Pm,一群消费者进程C1,C2,…,Ck,如图所示。假定生产者和消费者是相互等效,只要缓冲池未满,生产者就可以把产品送入缓冲区,类似地,只要缓冲池未空,消费者便可以从缓冲区取走产品并消耗它。生产者和消费者的同步关系将禁止生产者向满的缓冲池输送产品,也禁止消费者从空的缓冲池提取产品。
empty:说明空缓冲区的数目,其初值为缓冲池的大小n,表示消费者已把缓冲池中全部产品取走,有n个空缓冲区可用。
full:说明满缓冲区的数目(即产品数目),其初值为0,表示生产者尚未把产品放入缓冲池,有0个满缓冲区可用。
mutex: 说明该缓冲池是一临界资源,必须互斥使用,其初值为1。
“生产者—消费者”问题的同步算法描述:
semaphore full=0; /*表示满缓冲区的数目*/ semaphore empty=n; /*表示空缓冲区的数目*/ semaphore mutex=1; /*表示对缓冲池进行操作的互斥信号量*/ main() { cobegin producer(); consumer(); coend } producer() { while(true) { 生产一个产品放入nextp; P(empty); P(mutex); Buffer[in]=nextp; in=(in+1) mod n; V(mutex); V(full); } } consumer() { while(true) { P(full); P(mutex); nextc =Buffer[out]; out=(out+1) mod n; V(mutex); V(empty); 消费nextc中的产品; } }
“生产者-消费者”问题中应注意:
1. 互斥信号量的P、V操作在每一进程中必须成对出现。
2. 对资源信号量(full,empty)的P、V操作也必须成对出现,但可分别处于不同的程序中。
3. 多个P操作顺序不能颠倒。
4. 先执行资源信号量的P操作,再执行互斥信号量的P操作,否则可能引起进程死锁。
5. 它是一个同步问题:
(1)消费者想要取产品,缓冲池中至少有一个缓冲区是满的。
(2)生产者想要放产品,缓冲池中至少有一个缓冲区是空的。
6. 它是一互斥问题
缓冲池是临界资源,因此,各生产者进程和各消费者进程必须互斥访问。
用管程机制解决生产者—消费者问题
1、建立Producer-consumer(PC)管程
Type PC=monitor var in,out,count:integer; buffer:array[0,…,n-1] of item; notfull,notempty:condition; put(item); get(item); begin in:=out:=0; /* 初始化代码*/ count:=0; end
管程中的两个条件变量:
(1) notfull 表示等待未满缓冲区(空缓冲区)。
(2)notempty 表示等待未空缓冲区(满缓冲区)。
1、建立Producer-consumer(PC)管程
生产者利用此过程将自己生产的产品放到缓冲池中,若发现缓冲池已满(count n),则等待。
消费者利用此过程将缓冲池中的产品取走,若发现缓冲池已空(count 0),则等待。
put(item) Procedure entry put(item) begin if count >= n then notfull.wait; buffer(in):=nextp; in:=(in+1) mod n; count:=count+1; if notempty.queue then notempty.signal; end get(item) Procedure entry get(item) begin if count = 0 then notempty.wait; nextc:=buffer(out); out:=(out+1) mod n; count:=count-1; if notfull.queue then notfull.signal; end
2、生产者—消费者问题的解决
Producer:begin repeat produce an item in nextp; PC.put(item); until false end Consumer:begin repeat PC.get(item); consume the item in nextc; until false end
二、“哲学家进餐”问题
有五个哲学家,他们的生活方式是交替地进行思考和进餐。他们共用一张圆桌,分别坐在五张椅子上。在圆桌上有五个碗和五支筷子,平时一个哲学家进行思考,饥饿时便试图取用其左、右最靠近他的筷子,只有在他拿到两支筷子时才能进餐。进餐毕,放下筷子又继续思考。
哲学家进餐问题的解决:
semaphore stick[5]={1,1,1,1,1}; /*分别表示5支筷子*/ main() { cobegin philosopher(0); philosopher(1); philosopher(2); philosopher(3); philosopher(4); coend } //第i个哲学家的活动算法 philosopher(int i) { while(true) { 思考; P(stick[i]); P(stick[(i+1) mod 5]); 进餐; V(stick[i]); V(stick[(i+1) mod 5]); } }
说明:
1、此算法可以保证不会有相邻的两位哲学家同时进餐。
2、若五位哲学家同时饥饿而各自拿起了左边的筷子,这使五个信号量stick均为0,当他们试图去拿起右边的筷子时,都将因无筷子而无限期地等待下去,即可能会引起死锁。
上述解法可能出现永远等待,有若干种办法可避免死锁:
-
至多允许四个哲学家同时进餐(解决方法一);
-
奇数号先取左手边的筷子,偶数号先取右手边的筷子(解决方法二);
-
每个哲学家取到手边的两把筷子才进餐,否则一把筷子也不取(解决方法三)。
解决的方法(一)
while(true) { signal(stick[i]); wait(Sm); signal(stick[(i+1) mod 5]); wait(stick[i]); signal(Sm); wait (stick[(i+1) mod 5]); …... …… think; eat; } …...
解决的方法(二)
while(true) signal(stick[i]); {if odd(i) signal (stick[(i+1) mod 5]); {wait(stick[i]); …... wait (stick[(i+1) mod 5]); think; } …... else } {wait (stick[(i+1) mod 5]); wait(stick[i]); } …… eat; …...
对5个哲学家,假设规定:单号者进餐时,先拿左手(i)的筷子,然后再拿右手(i+1)的筷子。双号则先右后左。这样既可使5个人同时进餐,又不致产生死锁。
利用AND信号量机制解决哲学家进餐问题
semaphore stick[5]={1,1,1,1,1}; philosopher(int i) { while(true) { think; Swait(stick[(i+1) mod 5],stick[i]); eat; Ssignal(stick[(i+1) mod 5],stick[i]); } }
三、“读者—写者”问题
一个数据对象(数据文件或记录)可被多个进程共享。其中,读者(reader)进程要求读,写者(writer)进程要求写或修改。
为了保证读写的正确性和数据对象的一致性,系统要求:当有读者进程读文件时,不允许任何写者进程写,但允许多个读者同时读;当有写者进程写时,不允许任何其它写者进程写,也不允许任何读者进程读。
设置一个共享变量和两个信号量:
共享变量readcount:记录当前正在读数据集的读进程数目,初值为0。
读互斥信号量rmutex :表示读进程互斥地访问共享变量readcount,初值为1.
写互斥信号量wmutex:表示写进程与其它进程(读、写)互斥地访问数据集,初值为1.
“读者—写者”问题的同步算法描述
semaphore rmutex=1; semaphore wmutex=1; int readcount=0; main() { cobegin reader(); writer(); coend } reader() { while(true) { P(rmutex); if(readcount= =0) P(wmutex);/*第一位读者阻止写者*/ readcount++; //修改readcount V(rmutex); 读数据集; P(rmutex); readcount--; //修改readcount if(readcount= =0) V(wmutex);/*第末位读者允许写者进*/ V(rmutex); } } writer() { while(true) { P(wmutex); // 阻止其它进程(读、写); 写数据集; V(wmutex); // 允许其它进程(读、写); } }