1.同步与同步机制
- 著名的生产者--消费者问题是计算机操作系统中并发进程内在关系的一种抽象,是典型的进程同步问题。
- 生产者进程可以是计算进程、发送进程;
- 消费者进程可以是打印进程、接收进程等等。
- 解决好生产者--消费者问题就解决好了一类并发进程的同步问题
生产者-消费者案例
有一环形缓冲池,包含n个缓冲区(0~n-1)。有两组进程:一组生产者进程和一组消费者进程,生产者进程向空的缓冲区中放产品,消费者进程从满的缓冲区中取走产品
生产者-消费者问题
P1、P2(生产者线程)各生产出一个产品,系统共有两个产品
- 正确的结果:这两个产品应被放在buffer[0 ] (=‘A’)和buffer[1 ] (=‘B’)两个缓冲区中,最后in =2, counter = 2
- 实际上的结果: buffer[0 ] =‘B’, buffer[1 ] 和buffer[2 ] 均为空, in = 2, counter = 2
- 出错原因(1): in 、 out均为临界资源,应互斥访问,即生产者进程互斥访问in,消费者进程互斥访问out,它们均互斥访问buffer。一次只允许一个进程访问,对in 、 out 的访问不能被打断
- 出错原因(2):各类进程访问缓冲区的速率不同,要得到正确结果,需要调整并发进程的速度,这需要通过在进程间交换信号或消息来调整相互速率,达到进程协调运行的目的。这种协调过程称为进程同步 如:生产者需要在缓冲区中有空位才能生产; 消费者需要缓冲区中有产品才能消费;
同步机制
操作系统实现进程同步的机制,它通常由同步原语组成
常用的同步机制
- 信号量与PV操作
- 管程
- 消息传递
2.信号量与PV操作
将交通管制中多种颜色的信号灯管理交通的方法引入操作系统,让两个或多个进程通过特殊变量展开交互。
- 信号量(Semaphore) :一个进程在某一特殊点上被迫停止执行,直到接收到一个对应的特殊变量值。
- 进程可以使用P、V两个特殊操作来发送和接收信号
原语
原语是操作系统内核中执行时不可中断的过程,即原子操作
信号量特点
- 信号量起信号灯的作用;
- 操作系统中,信号量是用来表示物理资源的实体,信号量是一种软资源;
- 除赋初值外,信号量仅能由同步原语对其进行操作,没有任何其他方法可以检查和操作信号量。即只能有P、V操作改变信号量。
两个信号量的操作原语
- P操作原语: passeren,中文译为"通过"
- V操作原语: vrijgeven,中文译为"释放"
- 常用的其他符号有:P和V;up和down;signal和wakeup等
信号量+pv
利用信号量和P、V操作既可以解决并发进程的竞争问题,又可以解决并发进程的协作问题
3.信号量的分类
信号量按其用途分为两种
- 公用信号量:初值常常为1,用来实现进程间的互斥。
- 私有信号量:初值常常为可用资源数,多用来实现进程同步。拥有该信号量的一类进程可以对其执行P操作,而另一类进程可以对其执行V操作。
信号量按其取值分为两种
- 二元信号量:仅取0和1 ,主要用于解决进程互斥问题;
- 一般信号量:允许取值为非负整数,主要用于解决进程同步问题。
信号量的形式
随着信号量的发展,先后出现了以下几种形式的信号量:整型信号量、结构型信号量、信号量集机制
整型信号量
(1)数据类型:整型量S
(2)操作:
- 初始化
- 原子操作P(S)
- 原子操作V(S)
- 除初始化外,仅能通过P、V操作访问
(3)原子操作:操作不能被中断
(4) P(S)、V(S)操作定义
- P(S): while S≤0 do no-op
- S:=S-1;
- V(S): S:=S+1;
(5) S初值:一般取可用资源数目
缺陷:未遵循“让权等待”原则 若S=0,则进程便在while S≤0 do no-op无限循环进行等待,白白浪费CPU时间 为实现让权等待(当进程不能进入临界区,应该立即释放处理机,防止进程忙等待),引进了结构性信号量机制
结构型信号量
(1)解决问题:让权等待
(2)办法:等待时阻塞自己
(3)数据结构
typedef struct semaphore {
int value;//可用资源数
struct pcb *list ;//阻塞队列
}
P(S)、V(S)操作定义
procedure P(semaphore &s){
s. value-- ;
if (s . value < 0)block(s.list);
}
procedure V (semaphore &s){
s . value ++ ;
if (s . value ≤ 0)wakeup(s. list) ;
}
- P(s)和V(s)中的s为两个过程的共享变量
- s的取值:信号量s的初值在初始化时,根据其用途进行设置
- 推论1:s .value>=0时, s. value值表示还可以执行P而不会被阻塞的进程数,每执行一次P就意味着一次分配一个单位的资源
- 推论2:当 s. value<0,表示没有可用资源,请求该资源的进程被阻塞。 s. value的绝对值表示被阻塞的进程数,执行一次V就意味着释放一个资源。若s. value<0表示还有被阻塞的进程,需要唤醒一个被阻塞的进程,使他到就绪队列中排队
- 推论3:通常,P操作意味着请求一个资源,V操作意味着释放一个资源;特定条件下P操作代表挂起进程的操作,V操作代表唤醒被挂起进程的操作
AND型信号量
(1)解决问题
克服整型及记录型信号量一次仅能获得一个单位的一种临界资源(一个单位是指一个信号量+对应的pv操作),从而容易导致死锁(违背有限等待原则)的缺陷 (如果有多个信号量容易导致死锁)。
(2) AND 同步机制思想:对于进程在整个运行过程中需要的所有资源,要么一次性地全部分配给该进程,要么一个也不分
(3)数据结构:类同记录型信号量的数据结构
(4)操作定义
Swait(S1 , S2 , … , Sn )
if S1≥1 and … and Sn≥1 then
for i:=1 to n do
Si := Si -1;
endfor
else
将进程放在与Si相关联的阻塞队列,Si是最先发现小于1的,并且将进程的程序计数器置于Swait操作的开头;
endif
Ssignal(S1 , S2 , … , Sn )
for i:=1 to n do
Si := Si + 1;
将与Si相关联的阻塞队列中的所有进程移出放入到就绪队列;
endfor
信号量集
(1)解决问题: 某进程一次需要N个某类临界资源时,避免执行N次P操作 当资源量<下限值时不分配
(2)数据结构:信号量S,需求值d,下限值t
(3)操作定义
Swait(S1 , t 1,d1; … ; Sn , tn , dn )
if S1≥ t 1 and … and Sn≥ tn then
for i:=1 to n do
Si := Si - di;
endfor
else
将进程放在与Si相关联的阻塞队列,Si是最先发现小于ti的,并且将进程的程序计数器置于Swait操作的开头;
endif
Ssignal(S1 ,d1 , … , Sn ,dn )
for i:=1 to n do
Si := Si + di ;
将与Si相关联的阻塞队列中的所有进程移出放入到就绪队列;
endfor
4.信号量实现互斥
方法
为临界资源设一互斥信号量mutex,初值为1,将临界区置于P(mutex)和V(mutex)之间, P(mutex)和V(mutex)一定成对出现在同一个进程中
对于两个并发进程,互斥信号量的值仅取1、0和-1三个值
- 若MUTEX=1表示没有进程进入临界区
- 若MUTEX=0表示有一个进程进入临界区
- 若MUTEX=-1表示一个进程进入临界区,另一个进程等待进入。
5.信号量解决5位哲学家吃通心面问题
问题描述
五位哲学家围坐一张圆桌,桌上放五根筷子,每位哲学家只能拿起与他相邻的两根筷子吃饭 哲学家的生活方式是交替地进行思考和进餐
利用结构型信号量解决哲学家进餐问题
- 数据结构:每根筷子都是一个临界资源,都应定义一个信号量,为五根筷子定义一个信号量数组,每个信号量的初值为1
- 算法实现:
操作描述:第i位哲学家的活动如下:
Semaphore fork[5];
for (int i=0;i<5;i++) fork[i]=1;
Cobegin
Process philosopher_i(){
while(true){
think();
P(fork [ i ]);
P(fork [(i+1) mod 5]);
eating ;
V(fork [ i ]);
V(fork [(i+1)mod 5]);
thinking ;
}
}
coend
结构型信号量缺点
若每个哲学家都各自拿起他左边的一根筷子,然后再去拿他右边的筷子时,将都拿不到右边的筷子,大家又都不会放下手中的筷子,大家在相互等待别人释放筷子,系统于是进入死锁状态
死锁问题解决方案
1)至多允许有四位哲学家同时去拿左边的筷子,最终保证至少有一位哲学家能够进餐,(至多可以有两位哲学家能够进餐)
此时已经有四位哲学家拿起了筷子,所以哲学家4不能再拿筷子了,哲学家3可以拿起筷子4,从而拥有两根筷子可以吃饭
2)仅当哲学家的左右两根筷子均可用时,才允许他拿起筷子进餐。这种方案可以采用AND信号量机制来实现
3)规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子;偶数号哲学家先拿他右边的筷子,然后再去拿左边的筷子
利用AND型信号量解决哲学家进餐问题
Semaphore fork[5];
for (int i=0;i<5;i++) fork[i]=1;
Cobegin
Process philosopher_i(){
while(true){
think ;
Swait(fork[(i+1)mod 5],fork[i]);
eat ;
Ssignal(fork[(i+1) mod 5],fork[i]);
}
}
coend
5.信号量解决生产者-消费者问题
数据结构:
- 1)含有n个缓冲区的公用缓冲池
- 2)互斥信号量mutex:实现诸进程对缓冲池的互斥使 用,一次仅允许一个进程读或写公用缓冲池,初值为1
- 3)资源信号量empty:记录空缓冲区个数,初值为n
- 4)资源信号量full:记录满缓冲区个数,初值为0
操作要求:多个生产者进程之间、多个消费者进程之间、生产者进程与消费者进程之间均能正确同步
操作描述:
注意:
1)在每个程序中用于互斥的P(mutex)和V(mutex)必须成对出现。
2)对资源信号量empty和full的操作也同样必须成对出现,但它们在不同的程序中。
3)在每个程序中的多个P操作顺序不能颠倒,否则容易引起死锁 ,进程应先申请资源信号量p(empty)/p(full),再申请互斥信号量p(mutex)!
PV操作信号量的疑问
- s. value大于0那就表示有临界资源可供使用,为什么不唤醒进程?
S大于0的确表示有临界资源可供使用,而且这个时候没有进程被阻塞在这个资源上,也就是说没有进程因为得不到这类资源而阻塞,所以没有被阻塞的进程,自然不需要唤醒。
- s. value小于0应该是说没有临界资源可供使用,为什么还要唤醒进程?
V原语操作的本质在于:一个进程使用完临界资源后,释放临界资源,使S加1,以通知其它的进程,这个时候如果S<0,表明有进程阻塞在该类资源上,因此要从阻塞队列里唤醒一个进程来“接手”该类资源。比如,有两个某类资源,四个进程A、B、C、D要用该类资源,最开始S=2,当A进入,S=1,当B进入S=0,表明该类资源刚好用完, 当C进入时S=-1,表明有一个进程被阻塞了,D进入,S=-2。当A用完该类资源时,进行V操作,S=-1,释放该类资源,而这时S<0,表明有进程阻塞在该类资源上,于是唤醒一个。