(三)同步与互斥
间接相互制约关系(互斥):若某一进程要求使用某种资源,而该资源正在被另一个进程占用,并且该资源不允许两个资源同时占用,那么只能等占用资源的进程释放资源后再使用,这种制约关系为互斥关系,因其制约形式是“进程——资源——进程”,故称为间接相互制约关系。
直接相互制约关系(同步):某一进程若收不到另一个进程给他提供的必要信息就不能继续下去,这种情况表明了两个进程要在某些点上交换信息,相互交流的情况。这种制约形式是“进程——进程”,也因此称为直接相互制约关系(同步)。
Statement:同类进程即为互斥关系,不同类进程为同步关系。例如:生产者和生产者是互斥关系,生产者和消费者是同步关系。
1、临界资源与临界区
临界资源:同时仅允许一个进程使用或访问的资源称为临界资源,需要不同进程互斥访问。
临界区:进程中用于访问临界资源的一段代码,是属于对应的进程的。
2、信号量及同步原语
信号量是一个确定的二元组(s, q),其中s是一个具有非负初值的整型变量,q是一个初始状态为空的队列。整型变量s表示系统中某类资源的数目,当其值大于0时,表示系统中存在的可用资源数目,当其小于0时,表示系统中因请求该资源而被阻塞的进程的数目。除信号量的初值外,信号量的操作只能通过P操作和V操作来改变,操作系统利用它的状态对进程和资源进行管理。
P操作:P(s): s = s – 1 若s >= 0则继续,否则阻塞该进程,并将该进程挂在等待队列中。
struct semaphore{
int count;
queue q
};
P(s){
s.count--
if(s.count < 0){
该进程阻塞
将该进程插入阻塞队列s.q
}
}
V操作:V(s) : 若s > 0则继续,否则,从该进程等待队列中移出第一个进程,使其变为就绪状态,插入就绪队列当中,然后继续执行。
V(s){
s.count++
if(s.count <= 0){
在s.q中取出第一个进程p
将p插入就绪队列
}
}
注意:
P和V均为不可分割的原子操作,若执行P/V操作,必须执行完毕。
P和V在系统中一定是成对实现的但未必在同一个进程中。
实现进程同步:
考虑同步关系:并发进程P1和P2,P1中有一条语句S1,P2中有一条语句S2,则通过信号量实现进程同步如下:
semaphore N = 0
P1(){
...
S1
V(N)
...
}
P2(){
...
P(N)
S2
...
}
实现进程互斥:
考虑互斥问题:P1和P2共享资源S,且S只能同时供一个进程使用或访问,则通过信号量实现进程互斥过程如下:
semaphore N = 1
P1(){
...
P(N)
访问S
V(N)
...
}
P2(){
...
P(N)
访问S
V(N)
...
}
3、经典同步问题
生产者-消费者问题
问题描述:一组生产者向一组消费者提供产品,他们共享一个有界的缓冲区,生产者向其中投入产品,消费者取走产品,循环往复,以至无穷......
semaphore full = 0
semaphore empty= n
semaphore mutex = 1
Producer(){
while(1){
produce an item
P(empty) //获取空位置
P(mutex) //获取缓冲区访问权限,与上一行不可调换顺序,否则可能死锁
将产品放入缓冲池
V(mutex) //释放缓冲区访问权限
V(full) //占位
}
}
Consumer(){
while(1){
P(full) //获取产品位置
P(mutex) //获取缓冲区访问权限,与上一行不可调换顺序,否则可能死锁
取出产品
V(mutex) //释放缓冲区访问权限
V(empty) //空出一个位置
}
}
注意:
P(full)/P(empty)与P(mutex)顺序不能变换,否则可能出现死锁。例如,某一时刻缓冲区已经满,而生产者先P(mutex)成功,这时,由于empty = 0,故P(empty)会被挂起,生产操作不能进行,而消费者因为不能申请到对缓存区的访问也不能取走一个产品,故生产者和消费者会陷入相互等待,形成死锁。因此P操作必须后对互斥信号量进行操作。
mutex不能省略!例如,在生产者和消费者不唯一的情况下,两个生产者同时进行了P(empty)操作,第一个生产者执行了buffer(in) = nextp操作,这时,该生产者还未来得及执行in= (in + 1) % n 的操作,而第二个生产者以及执行了buffer(in) = nextp操作,这样就将两个新产品放在了同一个位置上,因此在多生产者和多消费者的情况下,一定要设置mutex互斥信号量,以保证对缓冲池的互斥访问。简而言之,互斥信号量就是为了同类进程互斥准备的。
哲学家进餐问题
问题定义:5个哲学家围桌而坐,每两个哲学家之间放一支筷子,哲学家们在思考和干饭之间切换状态,当他们想干饭时,他们拿起左右的筷子进餐。也就是每支筷子会被 其左右两个哲学家使用......
分析:该问题筷子是临界资源,不能同时被两个哲学家使用,首先可以通过如下信号量来控制互斥过程。
semaphore Fork[5] = {1, 1, 1, 1, 1}
philosopher(int i){
while(1){
think();
P(Fork[i % 5]) // 申请使用第1根筷子
P(Fork[(i + 1) % 5]) // 申请使用第2根筷子
ganfan()
V(Fork[i % 5])
V(Fork[(i + 1) % 5])
}
}
注意,这种算法存在问题,就是五个哲学家同时拿起左手边的筷子时,五个哲学家都不能拿起右手边的筷子,因此陷入交替等待的死锁状态。解决办法:同时只允许四个哲学家同时进餐,或者规定奇数号的哲学家每次先拿起左手边的筷子,偶数号的哲学家每次先拿起右手边的筷子。
每次只允许4个哲学家同时进餐
semaphore Fork[5] = {1, 1, 1, 1, 1}
flag = 4
philosopher(int i){
while(1){
think();
P(flag)
P(Fork[i % 5]) // 申请使用第1根筷子
P(Fork[(i + 1) % 5]) // 申请使用第2根筷子
ganfan()
V(flag)
V(Fork[i % 5])
V(Fork[(i + 1) % 5])
}
}
奇数号哲学家先拿左边筷子
semaphore Fork[5] = {1, 1, 1, 1, 1}
philosopher(int i){
while(1){
if(i % 2 == 0){
think();
P(Fork[i % 5]) // 申请使用左手边筷子
P(Fork[(i + 1) % 5]) // 申请使用右手边筷子
ganfan()
V(Fork[i % 5])
V(Fork[(i + 1) % 5])
}else{
think();
P(Fork[(i + 1) % 5]) // 申请使用右手边筷子
P(Fork[i % 5]) // 申请使用左手边筷子
ganfan()
V(Fork[i % 5])
V(Fork[(i + 1) % 5])
}
}
}
理发师问题
问题描述:理发店里有一位理发师、一把理发椅和若干供顾客等待的椅子(n个),若没有顾客理发,则理发师坐在理发椅上睡觉;当一个顾客到来时,他会唤醒理发师,若理发师正在给顾客理发,如果有空凳子,则顾客等待;否则顾客离开。要为理发师和顾客设计一段程序来描述其活动。
int chairs = n + 1;
semaphore ready = 0;
semaphore finish= 1;
semaphore mutex = 1;
barber(){
while(1){
P(ready); //看看有没有顾客,如果没有就阻塞
理发();
P(mutex); //理发结束,对chairs进行操作
chairs++;
V(mutex);
V(finish) //理发师空闲
}
}
customer(){
P(mutex) //申请更改chair
if(chairs > 0){
chairs--; //找个凳子
V(mutex); //释放mutex
V(ready); //多了个准备好的顾客
P(finish); //等待空闲的理发师来给理发
}else{
V(mutex); //释放mutex
}
}
读者-写者问题
问题定义:读者和写者共同访问一个共享文档,访问需遵循如下规律:
多个读者可以同时读文件
一次只能有一个写者写文件
写者写文件时不能有读者正在读文件
情况一:读者优先算法:
int readernumber = 0;
semaphore rmutex = 1; //读者需互斥访问readernumber对其进行修改
semaphore mutex = 1; //保证对数据区的写互斥
writer(){
while(1){
P(mutex)
write()
V(mutex)
}
}
reader(){
while(1){
P(rmutex) // 申请修改readernumber
if(readernumber == 0){
/* 如果此时readernumber为0,说明第一个读者刚要访问该共享文件,因此要禁止后面到来的写者进入,而若此前已有读者正在访问过程中,那么写者已经被阻塞
同时,如果已有写者进入,那么读者会被阻塞*/
P(mutex)
}
readernumber++ // 读者数量加1
V(rmutex) // 释放readernumber权限,允许其他读者访问之
read() // 进行读操作
P(rmutex) // 再次申请readernumber修改权限
readernumber-- // 读者数-1
if(readernumber == 0){
/** 如果是最后一个读者退出文件,释放mutex,允许后续写者进入 */
V(mutex)
}
V(rmutex) // 释放readernumber权限,允许其他读者访问之
}
}
这种情况是利于读者访问的,因为如果当前有两个读者进程在读文档,一个写者进程被阻塞,此时再来一个读者进程,那么这个读者进程将被允许读文档,而阻塞的写者进程只能在所有读者都退出进程才能被访问。
情况二:写者优先算法
semaphore rmutex = wmutex = r = w = 1
int readercount = 1
int writercount= 1
reader(){
while(1){
P(r)
P(rmutex)
if(readercount == 0){
P(w)
}
readercount++
V(rmutex)
V(r)
read()
P(rmutex)
if(readercount == 1){
V(w)
}
readercount--
V(rmutex)
}
}
writer(){
while(1){
P(wmutex)
if(writercount == 0){
P(r)
}
writercount++
V(wmutex)
P(w)
write()
V(w)
P(wmutex)
writercount--
if(writercount == 0){
V(r)
}
V(wmutex)
}
}
在本例中信号量来控制写者进程的进入,若有写着存在则占用该信号量,阻止后续读者进如临界区,而w信号量代表对临界区进行写操作的权力,当有读者占用临界区时,占用信号量w以阻止写者进行写操作。本解法的rmutex和wmutex信号量表示对读者、写者计数器互斥操作控制的信号量。
情况三:读者写者公平
semaphore mutex = rmutex = wmutex = 1
int readercount = 0
reader(){
while(1){
P(wmutex) //检测是否有写者
P(rmutex)
if(readercount == 0){
P(mutex)
}
readercount++
V(rmutex) //恢复rmutex
V(wmutex) //恢复wmutex
read()
P(rmutex) //申请readercount使用
readercount--
if(readercount == 0){
V(mutex) //释放数据区,允许写者进入
}
V(rmutex)
}
}
writer(){
while(1){
P(wmutex) //检测是否有其他写者存在,无写者时进入
P(mutex) //申请数据区访问
write() //进行写操作
V(mutex) //释放数据区,允许其他进程写
V(wmutex) //恢复wmutex
}
}