并发性:互斥&同步
参考文献
操作系统 —— 精髓与设计原理第八版(Operating systems: Internals and Design Principles, Eighth Edition , [美] Williams Stallings著,陈向群,陈渝 等译,P 136-146
信号量
- 两个或多个进程通过见到那的信号进行合作,强迫一个进程在某个位置停止,直到它接收到一个特定的信号;
- 为了发信号,使用变量 信号量s;
- 原语(原子操作):(1)传送信号,semSignal(s),也称P操作;(2)接收信号,semWait(s),也称V操作;
- 若相应的信号仍未发送,则阻塞进行,直到发送完为止.
信号量,一个值为整数的变量,整数值上定义3个操作:
- 一个信号量初始化成非负数
- semWait(s),信号量 -1 ,若值变成负数,则阻塞执行semWait的进程,否则继续执行
- semSignal(s),信号量 +1,若值小于等于0,则被semWait操作阻塞的进程解除阻塞
总过程:
- 开始时,信号量的值为0或整数:
(1)若值为正数,则值等于发出semWait操作后,可立即执行的进程的数量
(2)若值为0(原因为 初始化 or 已有进程在等待),则发出semWait操作的下一个进程会被阻塞(因为下一个进来后,先执行 -1,就变为了负值,则阻塞执行semWait的进程) - 之后,每个后续的semWait操作都会使信号量的负值更大(上文说了,semWait操作先进行-1操作),如果为负值,则负值等于正在等待解除阻塞的进程的数量
- 在信号量为负值的情况下,每个semSignal操作都会将等待进程中的一个进程解除阻塞(上文说过,semSignal执行+1操作)
三个重要结论:
- 在对信号量-1前,不知道该进程是否会被阻塞;
- 当进程对一个信号量+1后,会唤醒另一个进程,两个进程继续并发运行。而在一个单处理器系统中,无法知道哪个进程先运行;
- 向信号量发出信号后,不需要知道是否有另一个进程正在等待,被解除阻塞的进程数要么没有,要么为1.
信号量
struct semaphore{
int count; /信号量计数
queueType queue;
};
void semWait(semaphore s){
s.count--;
if(s.count<0){ /临界区已满
/*当前进程插入队列*/;
/*阻塞当前进程*/;
}
}
void semSignal(semaphore s){
s.count++;
if(s.count<=0){ /semSignal释放了资源,因为s.count<0,所以唤醒阻塞的进程,但此情况下,下个在执行semWait的进程都会被阻塞
/*把进程P从队列中移除*/;
/*把进程P插入就绪队列*/;
}
}
二元信号量
struct binary_semaphore{
enum{zero,one} value;
queueType queue;
};
void semWaitB(binary_semaphore s){
if (s.value ==one ){ /s.value==1表示临界区有一个空位,==0表示有0个空位
s.value=zero;
}
else{
/把当前进程插入队列/;
/阻塞当前进程/;
}
}
void semSignal(binary_semaphore s){
if(s.queue is empty()){
s.value = one;
}
else{
/把进程P从等待队列中移除/;
/把进程P插入就绪队列/;
}
}
互斥锁
与二元信号量相关的一个概念“互斥锁”(mutex)。互斥锁是一个编程标志位,用来获取和释放一个对象。
当需要的数据不能被分析或处理,进而导致在系统中的其他地方不能同时执行时,互斥被设置为锁定(mutex=0),用于阻塞其他程序使用数据。
当数据不再需要或程序运行结束时,互斥被设定为非锁定。
与二元信号量区别:
互斥量加多(设为0)的进程和为互斥量解锁(设为1)的进程必须是同一个进程。二元信号量进程加锁,可能由另一个进程来解锁
互斥
上图为使用s信号量解决互斥问题的方法。每个进程进入临界区前执行semWait(s),即先执行 -1操作,若s<0,进程阻塞;若值 =0 ,进程立即进入临界区,之后s不再为正,因而其他进程都不能进入临界区
使用信号量的互斥
/*program mutualexclusion*/
const int n =/进程数/;
semaphore s=1;
void P(int 1){
while(true){
semWait(s);
/临界区/;
semSignal(s);
/执行进程的其余部分/;
}
}
void main(){
parbegin(P(1),P(2), ... ,P(n));
}
生产者/消费者问题(producer/consumer)
是并发处理中最常见的一类问题:
有一个或多个生产者生产某种类型的数据(记录,字符),并放置在缓冲区中,有一个消费者从缓冲区中取数据,每次取一项;系统保证避免对缓冲区的重复操作,即在任何时候只有一个主体(生产者或消费者)可访问缓冲区。
特殊情况需要保证:当缓存已满时,生产者不会继续向其中添数据;当缓存为空时,消费者不会从中移走数据。
缓冲区是无限的
用抽象的术语,可以定义如下的生产者和消费者函数:
producer:
while(true){
/生产v/;
b[in] = v;
in++;
}
consumer:
while(true){
while(in <= out){ /写入(索引)没有取出(索引)时,不做任何事
/不做任何事/;
}
w = b[out]; /取数据
out++; /取数据索引+1
/消费/;
}
在开始进行之前应该确保生产者已经生产( in > out )
使用二元信号量实现这个系统的而高压,不处理索引 in 和 out ,而使用整型变量n=in-out 简单地记录缓冲区中数据项的个数。信号量s用于实施互斥,信号量delay用于迫使消费者在缓冲区为空时等待(delay)
此方法里,生产者可以在任何时候自由地向缓冲区中增加数据项。它在添加数据前执行semWaitB(s),在之后执行semSignalB(s),以阻止消费者或任何其他生产者在添加操作过程中访问缓冲区。
但可能产生死锁
解决方法:引入一个辅助变量,可以在消费者的临界区中设置这个变量供以后使用
使用二元信号量解决无限缓冲区生产者/消费者问题的正确方法
/*program producerconsumer*/
int n;
binary semaphore s = 1 , delay = 0;
void producer(){
while(true){
produce(); /执行程序
semWaitB(s); /信号量实施互斥,占用资源
append(); /添加数据
n++; /添加后缓冲区数据个数+1
if(n == 1){ /如果此时n=1 就释放delay信号 使消费者空时等待(刚写进去,还没热霍,防止出错)
semSignal(delay);
}
semSignalB(s); /此时释放资源
}
}
void consumer(){
int m; /局部变量
semWaitB(delay); /空时等待
while(true){
semWaitB(s); /实施互斥
take; /取数据
n--; /缓冲区数据数-1
m = n; /局部变量=现存的缓冲区数据数
semSignalB(s); /释放资源
consume(); /消费
if(m == 0){ /如果m==0()缓冲区数据数为空,就开始等待(空时等待)
semWaitB(delay);
}
}
}
void main(){
n=0;
parbegin (producer, consumer); /运行两个程序
}
使用信号量解决无限缓冲区生产者/消费者问题的方法
使用一般信号量(计数信号量),变量n为信号量,其值等于缓冲区中的项数
且操作semSignal(s)和senSignal(n)互换无影响,因为在cunsumer要这两个信号量都等到了才会继续下面的程序。
但semWait(s)和semWait(n)不能互换,因为如果先semWait(s),如果此时 n=0 ,s又加了锁,consumer,无法消费,无法释放锁,producer因为consumer的s锁也无法写进去,即consumer等待producer的写入,producer等待consumer的释放使用权s,系统就会发生死锁
/*program producerconsumer*/
semaphore n=0 , s=1;
void producer(){
while (true){
produce();
semWait(s); /等待信号量s,得到使用权
append(); /加入数据
semSignal(s); /释放使用权
semSignal(n); /信号量n=缓冲区中的项数,释放
}
}
void consumer(){
while(true){
semWait(n); /等n信号量
semWait(s); /等s信号量
take(); /取数据
semSignal(s); /释放资源
consume(); /消费
}
}
void main(){
parbegin (producer, consumer);
}
增加约束:缓冲区是有限的,缓冲区被视为一个循环存储器,指针值为按缓冲区的大小取模,总是表示如下关系
被阻塞 | 解除阻塞 |
---|---|
生产者:在满缓冲区中插入 | 消费者:移出一项 |
消费者:从空缓冲区中移出 | 生产者:插入一项 |
生产者和消费者函数可写为以下形式:
producer:
while(true){
/生产v/;
while((in+1) % n == out){ /满时就不做事
/不做任何事/;
}
b[in] = v; /不满时就写进去
in = (in + 1) % n; /更新in的值
}
consumer:
while(true){
while(in == (out+1) % n){
/不做任何事/;
}
w = b[out];
out = (out + 1) % n;
/消费w/;
}
使用信号量解决有限缓冲区生产者/消费者问题的方法
使用一般信号量的解决方案,增加了信号量e来记录空闲空间的数量
/*program boundedbuffer*/
const int sizeofbuffer = /缓冲区大小/;
semophore s = 1, n = 0, e = sizeofbuffer;
void producer(){
while(true){
produce(); /执行生产者程序
semWait(e); /等待buffer中空闲空间的数量的信号量
semWait(s); /锁住使用权
append(); /增加数据
semSignal(s); /释放使用权
semSignal(n); /发送buffer中有的数据量的值的信号量
}
}
void consumer(){
while(true){
semWait(n); /等待缓冲区中数据的数量n的信号量
semWait(s); /等待使用权
take(); /取走数据
semSignal(s); /释放使用权
semSignal(e); /发送现有的空闲空间的数量
}
}
void main(){
parbegin(producer,consumer);
}
信号量的实现
semWait 和 semSignal 操作需要为原子原语实现。问题的本质是互斥:任何时候只有一个进程可用semWait或semSignal操作控制一个信号量。可用硬件或软件的方案,但都增加额外开销。
对于单处理器系统,在semWait或semSignal操作期间可以禁用中断,因为这些操作的执行时间相对很短,因此这种方法是合理的
比较和交换指令(看不懂,看下面中断的看懂了也一样):
semWait(s){
while(compare and swap(s.flag,0,1) == 1){
/不做任何事/;
}
if (s.count < 0) {
/该进程进入s.queue队列/;
/阻塞该进程(还须将s.flag置0)/;
}
s.flag = 0;
}
中断:
semWait(s){
禁用中断;
s.count--; /先执行-1操作
if(s.count < 0){ / 执行-1操作后小于0就进队列阻塞
/该进程进入s.queue队列/;
/阻塞该进程,并允许中断/;
}
else{ /进入临界区,不能让其他的进程中断
/允许中断/; /但是可以自己要使用其他东西的时候就中断
}
}
semSignal(s){
禁用中断;
s.count++;
if (s.count <= 0){
/从s.queue队列中移出进程P/;
/进程P进入就绪队列/;
}
允许中断; /原子操作执行完毕,可以中断了
}