本文摘自汤小丹主编《计算机操作系统》(第三版)2.4 经典进程的同步问
在多道程序环境下,进程同步问题十分重要,也是相当有趣的问题,因而吸引了不少 学者对它进行研究,由此而产生了一系列经典的进程同步问题,其中较有代表性的是“生 产者—消费者”问题、“读者—写者问题”、“哲学家进餐问题”等等。通过对这些问题的研 究和学习,可以帮助我们更好地理解进程同步的概念及实现方法。
生产者—消费者问题
前面我们已经对生产者—消费者问题(The proceducer-consumer problem)做了一些描述, 但未考虑进程的互斥与同步问题,因而造成了数据(Counter)的不定性。由于生产者—消费者 问题是相互合作的进程关系的一种抽象,例如,在输入时,输入进程是生产者,计算进程 是消费者;而在输出时,计算进程是生产者,而打印进程是消费者。因此,该问题有很大 的代表性及实用价值。本小节将利用信号量机制来解决生产者—消费者问题。
1. 利用记录型信号量解决生产者—消费者问题
假定在生产者和消费者之间的公用缓冲池中,具有 n 个缓冲区,这时可利用互斥信号 量 mutex 实现诸进程对缓冲池的互斥使用。利用信号量 empty 和 full 分别表示缓冲池中空缓 冲区和满缓冲区的数量。又假定这些生产者和消费者相互等效,只要缓冲池未满,生产者 便可将消息送入缓冲池;只要缓冲池未空,消费者便可从缓冲池中取走一个消息。对生产 者—消费者问题可描述如下:
Var mutex,empty,full: semaphore:=1,n,0;
buffer:array[0,...,n-1] of item;
in,out: integer:=0,0;
begin
parbegin
proceducer: begin
repeat
...
producer an item nextp;
...
wait(empty);
wait(mutex);
buffer(in):=nextp;
in:=(in+1) mod n;
signal(mutex);
signal(full);
until false;
end
consumer: begin
repeat
wait(full);
wait(mutex);
nextc:=buffer(out);
out:=(out+1) mod n;
signal(mutex);
signal(empty);
consumer the item in nextc;
until false;
end
parend
end
在生产者—消费者问题中应注意:首先,在每个程序中用于实现互斥的 wait(mutex)和 signal(mutex)必须成对地出现;其次,对资源信号量 empty 和 full 的 wait 和 signal 操作,同 样需要成对地出现,但它们分别处于不同的程序中。例如,wait(empty)在计算进程中,而 signal(empty)则在打印进程中,计算进程若因执行 wait(empty)而阻塞,则以后将由打印进程 将它唤醒;最后,在每个程序中的多个 wait 操作顺序不能颠倒,应先执行对资源信号量的 wait 操作,然后再执行对互斥信号量的 wait 操作,否则可能引起进程死锁。
2. 利用 AND 信号量解决生产者—消费者问题
对于生产者—消费者问题,也可利用 AND 信号量来解决,即用 Swait(empty,mutex) 来代替wait(empty)和wait(mutex);用Ssignal(mutex,full)来代替signal(mutex)和signal(full); 用 Swait(full,mutex)来代替 wait(full)和 wait(mutex),以及用 Ssignal(mutex,empty)代替 Signal(mutex)和 Signal(empty)。利用 AND 信号量来解决生产者—消费者问题的算法描述 如下:
Var mutex,empty,full: semaphore:=1,n,0;
buffer:array[0,...,n-1] of item;
in out: integer:=0,0;
begin
parbegin
producer: begin
repeat
...
produce an item in nextp;
...
Swait(empty,mutex);
buffer(in):=nextp;
in:=(in+1)mod n;
Ssignal(mutex,full);
until false;
end
consumer:begin
repeat
Swait(full,mutex);
Nextc:=buffer(out);
Out:=(out+1) mod n;
Ssignal(mutex,empty);
consumer the item in nextc;
until false;
end
parend
end
3. 利用管程解决生产者—消费者问题
在利用管程方法来解决生产者—消费者问题时,首先便是为它们建立一个管程,并命 名为 ProclucerConsumer,或简称为 PC。其中包括两个过程:
- put(item)过程。生产者利用该过程将自己生产的产品投放到缓冲池中,并用整型变 量 count 来表示在缓冲池中已有的产品数目,当 count≥n 时,表示缓冲池已满,生产者须 等待。
- get(item)过程。消费者利用该过程从缓冲池中取出一个产品,当count≤0时,表示 缓冲池中已无可取用的产品,消费者应等待。
PC 管程可描述如下:
type producer-consumer=monitor
Var in,out,count: integer;
buffer: array[0, ..., n-1] of item; notfull,notempty:condition;
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
procedure entry get(item)
begin
if count<=0 then notempty.wait; nextc:=buffer(out);
out:=(out+1) mod n;
count:=count-1;
if notfull.quene then notfull.signal;
end
begin in:=out:=0;
count:=0
end
在利用管程解决生产者—消费者问题时,其中的生产者和消费者可描述为:
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
哲学家进餐问题
由 Dijkstra 提出并解决的哲学家进餐问题(The Dinning Philosophers Problem)是典型的同 步问题。该问题是描述有五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌 上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进 行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。 进餐完毕,放下筷子继续思考。
1. 利用记录型信号量解决哲学家进餐问题
经分析可知,放在桌子上的筷子是临界资源,在一段时间内只允许一位哲学家使用。 为了实现对筷子的互斥使用,可以用一个信号量表示一只筷子,由这五个信号量构成信号 量数组。其描述如下:
Var chopstick: array[0,...,4] of semaphore;
所有信号量均被初始化为 1,第 i 位哲学家的活动可描述为:
repeat
wait(chopstick[i]);
wait(chopstick[(i+1)mod 5]);
...
eat;
...
signal(chopstick[i]);
signal(chopstick[(i+1)mod 5]);
...
think;
until false;
在以上描述中,当哲学家饥饿时,总是先去拿他左边的筷子,即执行 wait(chopstick[i]);
成功后,再去拿他右边的筷子,即执行 wait(chopstick[(i+1)mod 5]);又成功后便可进餐。进 餐完毕,又先放下他左边的筷子,然后再放右边的筷子。虽然,上述解法可保证不会有两 个相邻的哲学家同时进餐,但有可能引起死锁。假如五位哲学家同时饥饿而各自拿起左边 的筷子时,就会使五个信号量 chopstick 均为 0; 当他们再试图去拿右边的筷子时,都将因 无筷子可拿而无限期地等待。对于这样的死锁问题,可采取以下几种解决方法:
- 至多只允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够 进餐,并在用毕时能释放出他用过的两只筷子,从而使更多的哲学家能够进餐。
- 仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐。
- 规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子,而偶数号哲学家则 相反。按此规定,将是 1、2 号哲学家竞争 1 号筷子;3、4 号哲学家竞争 3 号筷子。即五位 哲学家都先竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一位哲学家能获 得两只筷子而进餐。
2. 利用 AND 信号量机制解决哲学家进餐问题
在哲学家进餐问题中,要求每个哲学家先获得两个临界资源(筷子)后方能进餐,这在本 质上就是前面所介绍的 AND 同步问题,故用 AND 信号量机制可获得最简洁的解法。描述 如下:
Var chopsiick array of semaphore:=(1,1,1,1,1);
processi
repeat
think;
Sswait(chopstick[(i+1)mod 5],chopstick[i]);
eat;
Ssignat(chopstick[(i+1)mod 5],chopstick[i]);
until false;
读者—写者问题
一个数据文件或记录,可被多个进程共享,我们把只要求读该文件的进程称为“Reader 进程”,其他进程则称为“Writer 进程”。允许多个进程同时读一个共享对象,因为读操作不 会使数据文件混乱。但不允许一个 Writer 进程和其他 Reader 进程或 Writer 进程同时访问共 享对象,因为这种访问将会引起混乱。所谓“读者—写者问题(Reader-Writer Problem)”是 指保证一个 Writer 进程必须与其他进程互斥地访问共享对象的同步问题。读者—写者问题 常被用来测试新同步原语。
1. 利用记录型信号量解决读者—写者问题
为实现 Reader 与 Writer 进程间在读或写时的互斥而设置了一个互斥信号量 Wmutex。 另外,再设置一个整型变量 Readcount 表示正在读的进程数目。由于只要有一个 Reader 进 程在读,便不允许 Writer 进程去写。因此,仅当 Readcount=0,表示尚无 Reader 进程在读 时,Reader 进程才需要执行 Wait(Wmutex)操作。若 Wait(Wmutex)操作成功,Reader 进程便 可去读,相应地,做 Readcount+1 操作。同理,仅当 Reader 进程在执行了 Readcount 减 1 操作后其值为0时,才须执行signal(Wmutex)操作,以便让Writer进程写。又因为Readcount 是一个可被多个 Reader 进程访问的临界资源,因此,也应该为它设置一个互斥信号量 rmutex。
读者—写者问题可描述如下:
Var rmutex,wmutex: semaphore:=1,1;
Readcount: integer:=0;
begin
parbegin
Reader: begin
repeat
wait(rmutex);
if readcount=0 then wait(wmutex);
Readcount:=Readcount+1;
signal(rmutex);
...
perform read operation;
...
wait(rmutex);
readcount:=readcount-1;
if readcount=0 then signal(wmutex);
signal(rmutex);
until false; end
writer: begin
repeat
wait(wmutex);
perform write operation;
signal(wmutex);
until false; end
parend
end
2. 利用信号量集机制解决读者—写者问题
这里的读者—写者问题与前面的略有不同,它增加了一个限制,即最多只允许 RN 个读 者同时读。为此,又引入了一个信号量 L,并赋予其初值为 RN,通过执行 wait(L,1,1) 操作,来控制读者的数目。每当有一个读者进入时,就要先执行 wait(L,1,1)操作,使 L 的值减 1。当有 RN 个读者进入读后,L 便减为 0,第 RN+1 个读者要进入读时,必然会因 wait(L,1,1)操作失败而阻塞。对利用信号量集来解决读者—写者问题的描述如下:
Var RN integer;
L,mx: semaphore:=RN,1;
begin
parbegin
reader: begin
repeat
Swait(L,1,1);
Swait(mx,1,0);
...
perform read operation;
...
Ssignal(L,1);
until false;
end
writer: begin
repeat
Swait(mx,1,1;L,RN,0);
perform write operation;
Ssignal(mx,1);
until false;
end
parend end
其中,Swait(mx,1,0)语句起着开关的作用。只要无 writer 进程进入写,mx=1,reader 进 程就都可以进入读。但只要一旦有 writer 进程进入写时,其 mx=0,则任何 reader 进程就都无法进入读。Swait(mx,1,1;L,RN,0)语句表示仅当既无 writer 进程在写(mx=1),又无reader 进程在读(L=RN)时,writer 进程才能进入临界区写。
http://gurglessh.github.io/2016/04/12/经典进程的同步问题/