互斥和同步
这部分知识相对于后面的内存管理等要记忆的不多,主要是理解整个流程。
-
原子操作/临界区(一段用于访问共享资源的代码,不允许多个进程同时访问)/死锁/活锁/互斥/竞争条件
-
硬件的支持
- 中断禁用:只适用与单处理器机器中,一个进程在调用一个系统服务或被中断之前将一直运行,因此并发进程不能重叠,只能交替。由系统内核为启用和禁用中断定义的原语提供。进入临界区启用中断。
- 专用机器指令:执行期间任何其他指令访问内存都将被阻止,且这些动作在一个指令周期中完成
- compare_and_swap:使用测试值testval
- 忙等待/自旋等待:试图进入临界区的其他进程进入忙等待模式
/* program mutualexclusion */ const int n = /* number of processes */; int bolt; void P(int i) { while (true) { while (compare_and_swap(&bolt, 0, 1) == 1) /* do nothing */; /* critical section */; bolt = 0; /* remainder */; } } void main() { bolt = 0; parbegin (P(1), P(2), . . . ,P(n)); } /* program mutualexclusion */ int const n = /* number of processes*/; int bolt; void P(int i) { while (true) { int keyi = 1; do exchange (&keyi, &bolt) while (keyi != 0); /* critical section */; bolt = 0; /* remainder */; } } void main() { bolt = 0; parbegin (P(1), P(2), . . ., P(n));
-
信号量S:一个整型变量,除了初始化外只能通过原语wait()(P,荷兰语测试)和signal()访问(V,增加)
信号量的初值为可用资源数量。当进程需要使用资源时,需要对该信号量执行 wait() 操作(减少信号量的计数)。当进程释放资源时,需要对该信号量执行 signal() 操作(增加信号量的计数)。当信号量的计数为 0 时,所有资源都在使用中。之后,需要使用资源的进程将会阻塞,直到计数大于 0。
struct semaphore{//可用于控制访问具有多个实例的某种资源 int count; queueType queue; }; void semWait(semaphore s){//申请使用资源 s.count--;//在此之前是无法提前知道该信号量是否会被阻塞的 if(s.count<0){//==0表示当前进程可以使用最后一个空闲资源 //把当前进程插入队列 //阻塞当前进程 } } void semSignal(semaphore s){//结束使用资源 s.count++; if(s.count<=0){//原本至少是-1,至少有一个进程因等待而阻塞,则唤醒 //把进程P从队列中移除 //把进程P插入就绪队列 } }
解决同步问题的实例:现有两个并发运行的进程:P1 有语句 S1 而 P2 有语句 S2。假设要求只有在 S1 执行后才能执行 S2。我们可以轻松实现这一要求:让 P1 和 P2 共享同一信号量 synch,并且初始化为 0。
//进程P1中插入语句 S1; signal (synch); //在进程 P2 中,插入语句: wait (synch); S2; //因为 synch 初始化为 0,只有在 P1 调用 signal(synch) ,即 S1 语句执行之后,P2 才会执行 S2。
-
二元信号量:只能是0/1
struct binary_semaphore{//可用于控制访问具有多个实例的某种资源 enum {zero,one} value; queueType queue; }; void semWaitB(binary_semaphore s){//申请使用资源 if(s.value==one) s.value=zero;//被占领 else{//已经被占领 //把当前进程插入队列 //阻塞当前进程 } } void semSignalB(binary_semaphore s){//结束使用资源 if(s.queue is empty())//没人等待 s.value=one;//标志当前资源空闲 else{ //把进程P从队列中移除 //把进程P插入就绪队列 } }
-
互斥锁mutex:与二元信号量的区别在于加锁和解锁的进程必须是同一个。但它们都需要使用队列保存被阻塞进程,FIFO是强信号量,没规定顺序的是弱信号量。
-
解决互斥问题
const int n = N;//进程数 semaphore s=1;//第一个执行的进程将其置为0 //也可将其初始化成某个进程,表示允许多个进程同时进入临界区,s.count //s.count>=0:执行P而不被阻塞的进程数;<0:阻塞在队列中的进程数 void P(int i){ while(true){ semWait(S); //临界区 semSignal(s); //其余部分 } } void main(){ parbegin (P(1),P(2),P(3),P(4)); }
-
管程:为解决信号量信号量机制的缺点(进程自备同步操作,P(S)和V(S)操作大量分散在各个进程中,不易管理,易发生死锁),封装了同步操作,对进程隐蔽同步细节。写程序如同串行。
-
组成:局部于管程的共享变量;对数据结构进行操作的一组过程;对局部于管程的数据进行初始化的语句
-
c.wait( ):调用进程阻塞并移入与条件变量c相关的队列中,并释放管程,直到另一个进程在该条件变量c上执行signal( )唤醒等待进程并将其移出条件变量c队列。
-
c.signal( ):如果存在其他进程由于对条件变量c执行wait( )而被阻塞,便释放之;如果没有进程在等待,那么,信号被丢弃。
-
-
使用通知和广播的管程
-
消息传递:为了实现合作,进程之间需要交换信息。由原语
send(destination,message)
和receive(source,message)
实现。- 同步:
- 寻址:直接寻址/间接寻址(一/N对一/N)
- 消息格式
-
排队原则:FIFO/优先级
-
互斥
-
生产者/消费者问题:专门生产数据和取走数据的人,且保证缓冲区满不填入,空时不去出
-
假设缓冲区无限,且是一个线性数组
//双指针法 producer: while(true){ b[in]=v; in++; } consumer: while(true){ while(in<out) ;//do nothing w=b[out]; out++; //consume w }
-
二元信号量法
-
定义n=in-out,并辅以信号量delay用于迫使消费者在缓冲区为空时等待。
-
生产者:添加时执行P(s),之后V(s),保证只有自己访问,并将n++
-
n==1:填入前为空,则V(delay)通知消费者使用,消费者执行P(V),n–
-
可能引起错误结果,使用局部变量保存值进行改进
int n; binary_semaphore s=1,delay=0;//S为是否可访问,delay为是否可取 void producer(){ while(true){ produce(); semWaitB(s); append(); n++; if(n==1) semSignalB(delay); semSignalB(s); } } void consumer(){ int m;//改进!!局部变量 semWait(delay); while(true){ semWaitB(s); take(); n--; m=n;//改进!!确保保持了原本的n值 semSignalB(s); consume(); // if(n==0) semWaitB(delay); if(m==0) semWaitB(delay);//改进 } } void main(){ n=0; parbegin(producer,consumer); }
按照上面程序我们来分析一下整个过程修改前为什么可能引起错误:
生产者 消费者 s n delay 1 1 0 0 2 P(s) //进入临界区 0 3 n++ 1 4 if(n==1) V(delay) //通知当前可取 1 5 V(s) 1 6 //取走 P(delay) 0 7 P(s) 0 8 n– 0 9 V(s) 1 10 P(s) 0 11 n++ 1 12 if(n==1)V(delay) 1 13 V(s) 1 14 //该行动失败! if(n==0)P(delay) 15 P(s) 0 16 n– 0 17 V(s) 1 18 if(n==0)P(delay) 0 19 P(s) 0 20 n– -1 21 V(s) 1 分析一下第14行:
如果没有10-13行:消费者耗尽缓冲区数据后重置delay,if语句表示如果当前没有可用资源,P(delay)状态使消费者阻塞,等待生产者。
加入后:在检查n值之前生产者放入数据,并在V(delay)(通知不用等待)之前又率先执行if语句,导致在n=1的情况下判断消费者不需要阻塞,第20行就表示消费者已经消费了缓冲区中不存在的一项。
-
-
一般信号量法:
semaphore n=0,s=1; void producer(){ while(true){ produce(); semWait(s); append(); semSignal(s); semSignal(n); } } void consumer(){ while(true){ semWait(n); semWait(s); take(); semSignal(s); consume(); } }
-
-
缓冲区有限:视为循环存储器
semaphore n=0,s=1,e=Sizeofbuffer; void producer(){ while(true){ produce(); semWait(e); semWait(s); append(); semSignal(s); semSignal(n); } } void consumer(){ while(true){ semWait(n); semWait(s); take(); semSignal(s); semSignal(e); consume(); } }
-
-
PV操作必须作为原子原语实现:硬件或软件(Dekker/Peterson)
semWait(s) { while (compare_and_swap(s.flag, 0 , 1) == 1) /* do nothing */; s.count--; if (s.count < 0) { /* place this process in s.queue*/; /* block this process (must also set s.flag to 0)*/; } s.flag = 0; } semSignal(s) { while (compare_and_swap(s.flag, 0 , 1) == 1) /* do nothing */; s.count++; if (s.count <= 0) { /* remove a process P from s.queue */; /* place process P on ready list */; } s.flag=0; } semWait(s) { inhibit interrupts; s.count--; if (s.count < 0) { /* place this process in s.queue */; /* block this process and allow interrupts */; } else allow interrupts; } semSignal(s) { inhibit interrupts; s.count++; if (s.count <= 0) { /* remove a process P from s.queue */; /* place process P on ready list */; } allow interrupts; }
-
读者/写者问题
-
读者优先
int readcount; semaphore x=1,wsem=1; void reader(){ while(true){ semWait(x);//x用于确保readcount被更新和访问时不会被中途切断 readcount++; if(readcount==1) semWait(wsem);//第一个试图读的读进程在wsem上等待,至少有一个在用时不用等 /*如果初始时不能进入,唯一的可能就是有写进程在用,将置为了0,P后值为-1 直到写进程V后才为0,0表示有进程在等待队列中且当前有资源可用,故进入 之后进入的reader因为readcount>1,直接执行READUNIT()*/ semSignal(x); READUNIT(); semWait(x); readcount--; //直到读者全部退出才释放,故读者优先,写者要一直等待在wsem上,可能饥饿 if(readcount==0) semSignal(wsem); semSignal(x); } } void writer(){ while(true){ semWait(wsem);//写进程进入时其他的都不能用 WRITEUNIT(); semSignal(wsem); } }
-
写者优先(要求能背下来或者手写)
int readcount,writecount; semaphore x=1,y=1,z=1; semaphore wsem=1,rsem=1;//rsem:至少有一个写进程准备访问数据区时,用于禁止所有的读进程 void reader(){ while(true){ /*z只在reader部分使用,第一个读进程进入时z=0,当前该读进程成功获得读权利时我们发现 z被执行了V(z),相当于通知大家此时可以读,故此时其他读进程可以进入尝试执行读操作,若 第一个读进程被堵在了rsem上排队,则其他读进程堵在z上排队*/ //相当于为写进程设置了优先队列,如果没有z,则申请写的进程得排在之前已经申请过的读进程 //之后,相当于混合在一起排队 semWait(z); /*首先能进入该层的读者必然是第一个读者。如果此时没有写者,则该变量为1,成功获得 读资格,如果有写者在写或等待,因为写者执行P(rsem),则读者被堵在rsem上。*/ semWait(rsem); semWait(x); readcount++; if(readcount==1) /*如果是第一个进入缓冲区的读者,则对缓冲区进行上锁,防止其他写进程 试图进入打断,即申请的写进程堵在wsem上。如果readcount>1,说明此时 并没有写者申请或等待,不然下一顺位已经堵在rsem上了*/ semWait(wsem); //堵住写进程 semSignal(x); //该第一顺位已经进入缓冲区,故让原本的第二顺位读进程变成第一顺位,开始向下走 //能走到这一步必然是目前读进程已结束,且没有写进程在写或等待(否则rsem又-1) semSignal(rsem); semSignal(z);//依次放行,上下两行似乎可以调换 READUNIT(); semWait(x); readcount--; if(readcount==0) semSignal(wsem);//读者走了,释放资源,会优先被等待在wsem上的写进程抢到 semSignal(x); } } void writer(){ while(true){ semWait(y);//控制writecount的更新 writecount++; //第一个写着进入时获得rsem控制权,使得试图访问的第一顺位读进程被堵在rsem上 //如果已经有写者,说明读进程已经被堵,没必要重复设置,不加也行其实,就是麻烦 if(writecount==1) semWait(rsem); semSignal(y); semWait(wsem);//如果有读进程已经在读,则堵塞,写进程都堵在这里 WRITEUNIT(); semSignal(wsem); semWait(y); writecount--; if(writecount==0) semSignal(rsem);//最后一个写进程退出时才放开对rsem的限制 semSignal(y); } }
- 系统中只有读进程:设置wsem,无队列
- 系统中只有写进程:设置wsem和rsem,写进程在wsem上排队
- 读+写,读优先:由读进程设置wsem,写进程设置rsem,写进程都在wsem上排队,一个读进程在rsem上排队,其他读进程在z上排队
- 读+写,写优先:由写进程设置wsem,写进程设置rsem,写进程都在wsem上排队,一个读进程在rsem上排队,其他读进程在z上排队
-
消息传递来实现写者优先
void reader(int i){ message rmsg; while(true){ rmsg=i; send(readrequest,rmsg); receive(mbox[i],rmsg); READUNIT(); rmsg=i; send(finished,rmsg); } } void writer(int j){ message rmsg; while(true){ rmsg=j; send(writerequest,rmsg); receive(mbox[j],rmsg); WRITEUNIT(); rmsg=j; send(finished,rmsg); } } void controller(){ while(true){ if(count>0){ if(!empty(finished)){ receive(finished,msg); count++; } else if(!empty(writerequest)){ receive(writerequest,msg); writer id=msg.id; count=count-100; } } if(count==0){ send(writer id,"OK"); receive(finished,msg); count=100; } while(count<0){ receive(finished,msg); count++; } } }
有三个信箱,每个信箱存放一种它可能接收到的消息。
参考读物:《信号量小书》和附录A