zoukankan      html  css  js  c++  java
  • 正襟危坐说--操作系统(陆):进程同步

    进程同步

    引入线程后,我们也引入了一个巨大的问题:即多线程程序的执行结果有可能是不确定的。同步就是让所有线程按照一定的规则执行,使得其正确性和效率都有迹可循。线程同步的手段就是对线程之间的穿插进行控制。

     

    两个步骤(两条语句)中间留有被别的线程穿插的空挡,可能造成执行结果的错误。这时,我们可以用来将这两个步骤并为一个步骤,或者变成一个原子操作,使其中间不留空挡。

    锁有两个基本操作:闭锁和开锁。闭锁就是将锁锁上,其他人进不来。开锁就是你做的事情做完了,将锁打开,别人可以进去了。

    闭锁操作有两个步骤:

    ①等待锁为打开状态

    ②获得锁并锁上

    显然,闭锁的两个操作应该是原子操作,不然就会留下穿插的空挡,从而造成功效的丧失。

    锁的实现

    操作系统之所以能够构建锁之类的同步原语,原因就是硬件已经为我们提供了一些原子操作:中断禁止和启用、内存加载和存入、测试和设置指令。在这些硬件原子操作之上,我们便可以构建软件原子操作。

     

    锁的特征

    一把正常的锁应该具有的特征:

    ①锁的初始状态是打开状态

    ②进入临界区前必须获得锁

    ③出临界区时必须打开锁

    ④如果别人持有锁,则必须等待

    把锁机制想象成现实中的锁,闭锁和开锁中间的代码,想象成是放在有锁的房间里进行的。

     

    缺点:

    当一个进程持有锁时,另一个进程只能等待(等待锁变为打开状态),而这种繁忙等待就造成了浪费,也降低了系统效率。锁的特性就是在别人持有锁的情况下需要等待

    故:尽量减少闭锁开锁之间的代码(一般用锁来设置标记,保护临界变量的操作)

    有什么办法不用进行任何繁忙等待呢?睡觉与叫醒

     

    睡觉与叫醒

    什么是睡觉与叫醒?就是如果锁被对方持有,你不用等待锁变为打开状态,而是睡觉去,锁打开后再来把你叫醒。

    例:生产者与消费者问题(有问题版本)

    #define N 100  //缓冲区的大小
    int  count = 0 ; //缓冲区中产品的数目
     
    void producer(void)
    {
        int  item ;
        while (true)
        {
            if(count == N)         //若缓冲区满,则睡觉
               sleep() ;
                  item= produce_item() ; //若缓冲区未满,则生产一个产品
           insert_item(item) ;      //把产品放入
            count++;
            if(count == 1)         //若之前缓冲区是空的,则唤醒消费者
               wakeup(consumer) ;
        }//while
    }
     
    void consumer(void)
    {
        int  item ;
        while (true)
        {
            if (count == 0)         //若缓冲区空,则睡觉
               sleep() ;
            item =remove_item() ;  //若缓冲区非空,则拿走一件产品
            count--;
                  consume_item(item);   //消耗产品
            if(count == N-1)       //若之前缓冲区是满的,则唤醒生产者
               wakeup(productor) ;
        }//while
    }
     


    此代码有些问题:

    1,  变量count没有保护。可以通过在count操作的前后加锁lock和unlock来解决。(我们不喜欢锁的繁忙等待,因而发明了sleep&wakeup原语。这样在需要等待时我们去睡觉。但是,我们不喜欢等待,并不是一点都不能等待。只要等待的时间很短,我们是可以接受的)

    2, 当生产者在判断count==1时发送叫醒信号给消费者,但也有可能没有消费者在睡觉(当缓冲区为空时,恰好没有消费者来),这个信号放空了。没有进程接收。同样,消费者也存在放空信号叫醒生产者的问题。

    信号放空问题。可能造成死锁。

    ★假定consumer先来,这个时候count==0,于是睡觉去,但是在判断count等于0后却在执行sleep语句前CPU发生切换。生产者开始运行,它生产一件产品后,给count加一,发现count结果为1,因此发出叫醒消费者信号。但这个时候消费者还没有睡觉(正准备要睡),所以该信号没有任何效果,浪费了。而生产者一直运行到缓冲区满了后也去睡觉。这个时候CPU切换到消费者,而消费者执行的第一个操作就是sleep。至此,生产者和消费者都进入睡觉状态,从而无法相互叫醒,死锁。

    造成二者同时睡觉的原因是因为生产者发出的叫醒信号丢失(因为消费者此时还没睡觉)。

    如果用某种方法将发出的信号累积起来,而不是丢掉,在消费者获得CPU执行sleep语句后,生产者在这之前发送的信号还保留,则消费者将马上获得这个信号而醒过来。

    能够将信号累积起来的操作系统原语就是信号量

     

    信号量

    信号量可以说是所有通信原语里功能最强大的。它不光是一个同步原语,还是一个通信原语。而且,它还能作为锁来使用

    信号量semaphore实质上就是一个计数器。其取值为当前累积的信号数量。它支持两个操作:加法Up和减法Down。

    Down减法操作:

    ①判断信号量的取值是否大于等于1

    ②如果是,将信号量的值减1,继续往下执行

    ③否则,在该信号上等待(线程被挂起)

    Up加法操作:

    ①将信号量的值增加1(此操作将叫醒一个在该信号上等待的线程)

    ②线程继续往下执行

    注意,Down和Up操作虽然包含多个步骤,但这些步骤是一组原子操作,它们之间是不能分开的。

    Down和Up也被称为P、V操作。P和V是荷兰语中的减少、增加的意思。

    如果我们将信号量的取值限制为0和1两种情况,则我们获得的就是一把锁,也称为二元信号量

    二元信号量Down减法操作:

    ①等待信号量取值变为1

    ②将信号量值设置为0

    ③继续往下执行

    二元信号量Up加法操作:

    ①将信号量值设置为1

    ②叫醒在该信号量上面等待的第1个线程

    ③继续往下执行

    二元信号量的取值只有0和1,它可以防止任何两个程序同时进入临界区。

    ★二元信号量具备锁的功能,Down就是获得锁,Up就是释放锁。但它又比锁更为灵活:因为等在信号量上的线程不是繁忙等待,而是去睡觉,等另外一个线程执行Up操作来叫醒。因此,二元信号量从某种意义上说就是睡觉与叫醒两种原语操作的合成。

     

    例:用信号量解决生产者和消费者问题。

    (信号量:就是 资源数+睡觉唤醒机制)

    #define N 100            //定义缓冲区大小
    typedef int semaphore; 
    semaphore mutex = 1;   //互斥信号量
    semaphore empty = N;   //缓冲区计数信号量,用来计数缓冲区里的空位数量,初始允许N个生产者生产
    semaphore full = 0;       //缓冲区计数信号量,用来计数缓冲区里的产品数量
     
    void producer(void)
    {
        int  item ;
        while (true)
        {
            item =produce_item();
           down(&empty);      //获得一个许可生产的信号,否则睡眠
           down(&mutex);      //二元信号量,起锁的作用
           insert_item(item);
           up(&mutex);
           up(&full);           //发射一个信号量,允许一个进程来消费产品(唤醒等待在该信号上的线程)
        }
    }
     
    void consumer(void)
    {
        int  item ;
        while (true)
        {
            down(&full);          //获得一个许可消费的信号,否则睡眠
           down(&mutex);       //二元信号量,起锁的作用
            item =remove_item();
           up(&mutex);
           up(&empty);
           consume_item(item); //发射一个信号量,允许一个进程来生产产品(唤醒等待在该信号上的线程)
        }
    }

    生产者和消费者等待的信号不同,它们需要睡在不同的信号上,故设置一个full和一个empty来分别记录。

     

    锁、睡觉与叫醒、信号量

    锁解决了同步问题,但带来的是循环等待。为了消除循环等待,我们发明了睡觉与叫醒。但睡觉与叫醒又带来了死锁,因此我们发明了信号量。

    (P、V操作把睡觉与叫醒的if(……) sleep ; cnt++; 语句结合在一起成原子操作,消除了语句之间的空挡)

    (睡眠与唤醒是直接针对关联的进程操作的,而信号量机制是针对信号量操作的,进程在相应信号量上获取、增添,即不针对某一进程发送信号。)

    (线程同步的代码 不要有判断状态标记的if语句,最好把这些状态转换成信号量,让信号量操作的系统原语去判断,做出反应。这样可防止信号丢失,降低死锁的可能。)

     

    使用信号量原语时,信号量的操作顺序至关重要。稍有不慎,就可能发生死锁。

     

    管程

    为解决信号量使程序编写困难、程序效率低下的问题,我们将这些组织工作交给一个专门的构造来管,则程序员就解脱了。管程即监视器的意思。它监视的是进程或线程的同步操作。具体说来,管程就是一组子程序、变量和数据结构的组合。把需要同步的代码用一个管程的构造框起来,将需要保护的代码置于begin monitor 和 end monitor之间,即可获得同步保护。编译器来保证它的正确运行。

    管程最大的问题就是对编译器的依赖。我们需要编译器将需要的同步原语加在管程的开始和结尾。实际上,多数的程序设计语言也并没有实现管程机制。

     

    消息传递

    消息传递是通过同步双方经过互相收发消息来实现。它有两个基本操作,发送send和接收receive。它们均是操作系统的系统调用,而且既可以是阻塞调用,也可以是非阻塞调用。

    --send(destination,  &message)

    --receive(source,  &message)

    例:使用消息传递实现 生产者与消费者同步

    #define N 100
    void producer(void)
    {
        int item ;
        message m;   //消息框
        while (true)
        {
            item =produce_item() ;
           receive(consumer, &m) ;     //等待空消息框。等待消费者给它的空盒子
           build_message(&m, item) ;   //把产品放入空盒子内
           send(consumer, &m) ;        //把装有产品的盒子发送给消费者
        }
    }
     
    void consumer(void)
    {
        int item ;
        message m ;
        for (i=0;i<N; i++)
           send(producer, &m) ;        //发送N个空盒子
        while (true)
        {
           receive(producer, &m) ;     //接受含有产品的盒子
            item =extract_item(&m) ;   //把产品从盒子中取出
           send(producter, &m) ;       //把空盒子发给生产者
           consumer_item(item) ;
        }
    }

    同步需要的是阻塞调用。即如果一个线程执行receive操作,就必须等待收到消息后才能返回,也就是说,如果调用receive,则该线程将被挂起,在收到消息后,才能转入就绪。

    生产者每生产一件产品,就需要从消费者那里获取一个空盒子,然后将产品装进盒子里,再把装了产品的盒子发送给消费者。消费者的工作过程刚好反过来。生产者和消费者就这样通过消息的传递进行同步,既不会死锁,也不会繁忙等待。而且无需使用临界区等机制。

    (消息传递机制与信号量不同,它是直接与相关线程通信,而信号量机制是不同信号量之间交互 进程挂靠在不同的信号上)

    消息传递还可以跨计算机进行同步,即可以对处于不同计算机上的线程实现同步。故,消息传递是当前使用非常普遍的线程同步机制。

     

  • 相关阅读:
    Spring Security(06)——AuthenticationProvider
    Spring Security(05)——异常信息本地化
    Spring Security(04)——认证简介
    Spring Security(03)——核心类简介
    Spring Security(02)——关于登录
    Spring Security(01)——初体验
    核心服务
    技术概述
    Security命名空间配置
    Spring Security
  • 原文地址:https://www.cnblogs.com/riasky/p/3430761.html
Copyright © 2011-2022 走看看