• 操作系统学习笔记(二) 信号量、条件变量、互斥量、读写锁


    在有了进程和线程的模型之后,一个很大的问题就摆在眼前:进程和线程的执行顺序是不可预知的,那么,如何使得两个进程按照我们想要的顺序执行,从而得出正确的结果呢?

    竞争条件:两个或者多个进程读写某些共享数据,最后的结果依赖于进程运行的精确时序。

    临界区:把对共享内存进行访问的程序片段称作临界区。如果能使两个进程不可能同时处于临界区内,就能够避免竞争。

    先引入一个经典的进程同步问题:生产者-消费者问题

    生产者-消费者问题:有一个缓冲区,一个(或多个)进程在生产某种产品,它生产的东西会放入缓冲区内;一个(或多个)进程在消费产品,它会从缓冲区内取走产品。当缓冲区满时,生产者应当暂时停止生产;当缓冲区为空时,消费者应当暂时停止消费。

    很显然,这个问题用简单的判断缓冲区是否为0或N是无法解决的。如果在消费者判断缓冲区为0时,恰好遇到了进程切换,生产者进程开始运行,此时应当唤醒消费者,然而这个信号丢失了,因为切换到消费者才进行了睡眠。这时,生产者会不断运行,直到缓冲区满,两个进程全部睡眠,造成了死锁。代码如下:

    #define N 1000
    int count=0;
    void producer(void)
    {
        int item;
        while(TRUE)
        {
            item=produce_item();
            if(count==N) sleep();//一段时间后,缓冲区满,生产者进程也睡眠了
            insert_item(item);
            count=count+1;
            if(count==1) wakeup(consumer);//设想判断条件成立时,切换了进程,再次切回时,唤醒消费者进程,然而消费者进程此时没有睡眠,信号丢失
        }        
    }
    
    void consumer(void)
    {
        int item;
        while(TRUE)
        {
            if(count==0) sleep();//第一次count=1,消费者进程不会睡眠;第二次确实睡眠了
            item=remove_item();
            count=count-1;//此时缓冲区确实为空了
            if(count==N-1) wakeup(producer);
            consume_item(item);
        }
    }

    一、信号量

    信号量是一种数据结构,可以理解为一个用来计数的整数和一个队列。整数用来记录唤醒次数,而队列被用来记录因为该信号量而阻塞的进程。

    信号量只支持两种操作:P/V操作

    P操作,可以理解为测试并减一。P(signal1),如果signal1大于0,那么把它减一,进程继续执行;如果signal为0,那么执行P操作的进程将会被阻塞,从而变为阻塞态,添加到因为signal1信号而阻塞的进程队列中。

    V操作,可以理解为+1并唤醒。V(signal1)后,如果signal1本来就大于0,那么执行+1;如果有进程在该信号量上被阻塞,那么从队列中根据某种策略选择一个进程唤醒。如果多个进程在该信号量上阻塞,那么V操作后,signal1仍然可能为负数。

    需要注意的是,P/V操作均应当是原子操作,即作为一个整体执行而不会被打断。

    有了信号量,我们再来看生产者-消费者问题:

    #define N 1000
    typedef int semaphore;
    semaphore mutex=1;//控制对临界区的访问,其实就是互斥量
    semaphore empty=N;//表示空槽的数量
    semaphore full=0;//填满的槽的数量
    int count=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来表示已用和未用的数量,这样不管是满了还是空了,都不会造成死锁的问题。mutex的操作就是我们接下来要介绍的互斥锁。

    二、互斥锁

    互斥量其实可以理解为一个简化的信号量,它只有两种状态:0和1。互斥锁是用来解决进程(线程)互斥问题的。所谓进程互斥,就是两个进程实际上是一种互斥的关系,两者不能同时访问共享资源。

    互斥量和信号量原理比较类似,一旦一个线程获得了锁,那么其它线程就无法访问共享资源,从而被阻塞,直到该线程交还出了锁的所有权,另外一个线程才能获得锁。

    互斥锁的例子就不再给出,上面程序中已经有了,下面的程序中也会出现。

    三、条件变量

    条件变量是另外一种同步机制,可以用于线程和管程中的进程互斥。通常与互斥量一起使用。

    条件变量允许线程由于一些暂时没有达到的条件而阻塞。通常,等待另一个线程完成该线程所需要的条件。条件达到时,另外一个线程发送一个信号,唤醒该线程。

    条件变量对应的一组操作是pthread_cond_wait和pthread_cond_signal。

    条件变量与互斥量一起使用,一般情况是:一个线程锁住一个互斥量,然后当它不能获得它期待的结果时,等待一个条件变量;最后另外一个线程向它发送信号,使得它可以继续执行。

    需要注意的是,pthread_cond_wait会暂时解开持有的互斥锁。

    四、读写锁

    读写锁相对上面的问题会复杂一些,它被用来解决一个经典的问题:读者-写者问题

    读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态要么是不加锁状态,而且一次只有一个线程可以对其加锁。

    下面的代码考虑的是读者优先的读者-写者问题,对于共享区域的读写规则如下:

    1.只要有一个读者在读,后来的读者可以进入共享区直接读。

    2.只要有一个读者在读,写者就必须阻塞,直到最后一个读者离开。

    3.不考虑抢占式,写者在写时,即使有读者到达,也会在就绪态等待。

    typedef int semaphore;
    semaphore mutex=1;    //互斥锁,控制对rc的访问
    semaphore db=1;        //控制对数据库的访问
    int rc=0;        //当前读者计数
    
    void reader(void)
    {
        while(TRUE)
        {
            down(&mutex);//加锁
            rc=rc+1;
            if(rc==1) down(&db);//第一个读者,加锁
            up(&mutex);
            read_data_base();
            down(&mutex);
            rc=rc-1;
            if(rc==0) up(&db);//最后一个读者离开,解锁
            up(&mutex);
            use_data_read();
        }
    }
    
    void writer(void)
    {
        while(TRUE)
        {
            think_up_data();
            down(&db);//获取数据库访问的锁
            write_data_base();
            up(&db);
        }
    }

    这里,我们其实用了两个互斥锁来实现了读写锁。一个互斥锁用来保护共享区,另外一个互斥锁用来保护读者计数器。

    读写锁可以由三种状态:读模式下加锁状态写模式下加锁状态不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。

    在读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须阻塞直到所有的线程释放读锁。虽然读写锁的实现各不相同,但当读写锁处于读模式锁住状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。

    读写锁非常适合于对数据结构读的次数远大于写的情况。当读写锁在写模式下时,它所保护的数据结构就可以被安全地修改,因为当前只有一个线程可以在写模式下拥有这个锁。当读写锁在读状态下时,只要线程获取了读模式下的读写锁,该锁所保护的数据结构可以被多个获得读模式锁的线程读取。

    读写锁也叫做共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的;当他以写模式锁住时,它是以独占模式锁住的。

    五、总结

    这里,主要是简单总结一下这几种同步量的用法。

    1、互斥锁只用在同一个线程中,用来给一个需要对临界区进行读写的操作加锁。

    2、信号量与互斥量不同的地方在于,信号量一般用在多个进程或者线程中,分别执行P/V操作。

    3、条件变量一般和互斥锁同时使用,或者用在管程中。

    4、互斥锁,条件变量都只用于同一个进程的各线程间,而信号量(有名信号量)可用于不同进程间的同步。当信号量用于进程间同步时,要求信号量建立在共享内存区。

    5、互斥锁是为上锁而优化的;条件变量是为等待而优化的; 信号量既可用于上锁,也可用于等待,因此会有更多的开销和更高的复杂性。

    参考书籍:《现代操作系统》

  • 相关阅读:
    构建可靠的系统
    netty详解之reactor模型
    netty详解之io模型
    小明的魔法调度框架之旅
    JAVA版-微信高清语音.speex转.wav格式
    Spring Data JPA 缓存结合Ehcache介绍
    @media print样式 关于table断页
    JBPM学习第6篇:通过Git导入项目
    JBPM学习第5篇:Mysql配置
    JBPM学习第4篇:10分钟熟悉Eclipse
  • 原文地址:https://www.cnblogs.com/lustar/p/7689059.html
Copyright © 2020-2023  润新知