详细解读【互斥和同步】

互斥和同步

这部分知识相对于后面的内存管理等要记忆的不多,主要是理解整个流程。

  • 原子操作/临界区(一段用于访问共享资源的代码,不允许多个进程同时访问)/死锁/活锁/互斥/竞争条件

  • 硬件的支持

    • 中断禁用:只适用与单处理器机器中,一个进程在调用一个系统服务或被中断之前将一直运行,因此并发进程不能重叠,只能交替。由系统内核为启用和禁用中断定义的原语提供。进入临界区启用中断。
    • 专用机器指令:执行期间任何其他指令访问内存都将被阻止,且这些动作在一个指令周期中完成
      • 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)

    在这里插入图片描述
    在这里插入图片描述

    • 消息格式

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9QIgFwqc-1648553441764)(C:\Users\86130\AppData\Roaming\Typora\typora-user-images\image-20220329130000829.png)]

    • 排队原则: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);
        }
        

        按照上面程序我们来分析一下整个过程修改前为什么可能引起错误:

        生产者消费者sndelay
        1100
        2P(s) //进入临界区0
        3n++1
        4if(n==1) V(delay)//通知当前可取1
        5V(s)1
        6//取走P(delay)0
        7P(s)0
        8n–0
        9V(s)1
        10P(s)0
        11n++1
        12if(n==1)V(delay)1
        13V(s)1
        14//该行动失败!if(n==0)P(delay)
        15P(s)0
        16n–0
        17V(s)1
        18if(n==0)P(delay)0
        19P(s)0
        20n–-1
        21V(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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值