• Linux中的锁,条件变量


    为了保证多线程能够正确的运行,于是有了锁,锁是为了解决线程对临界资源的互斥访问而生的机制。

    互斥锁

    互斥锁是最简单的同步机制。进程要访问加锁的资源前先要获得锁,若锁未被占用,即获得并占用,用完释放锁。若线程访问时锁已被占用,则由调度器阻塞该进程,直到锁可用并且被调度使用。阻塞不占用CPU。

    由于锁也是多线程的一部分,因此头文件是 pthread.h,互斥锁相关的API如下:

     pthread_mutex_t mutex;            /*创建锁*/
     pthread_mutex_destroy(&mutex);    /*销毁锁*/
     pthread_mutex_init(&mutex,NULL);  /*创建后初始化才能使用*/
     pthread_mutex_lock(&mutex);       
     pthread_mutex_trylock(&mutex);    
     pthread_mutex_timedlock(&mutex, &timeout);  
     pthread_mutex_unlock(&mutex);     /*释放锁*/

    互斥锁有三种获得方式,阻塞调用 lock,如果调用时锁已被占用,线程就会被阻塞,加入到这个锁的排队队列中。非阻塞调用 trylock,只是尝试一下获取锁,如果没人用就用,有人用了就不用。超时阻塞调用 timedlock,还是阻塞调用,但是设定一个最大阻塞时间,超过时间就不用了。

    自旋锁

    自旋锁的用法和互斥锁一样,只是在原理上稍有不同,导致它们的应用场景也大有不同。当锁被占用,另一个进程尝试获取锁时,不想互斥锁会将进程阻塞,自旋锁是让进程一直循环询问锁是否可用。这时CPU仍然一直被该进程占用。相应的API使用如下。

     pthread_spinlock_t spin;        /*创建锁*/
     pthread_spin_destroy(&spin);    /*销毁锁*/
     pthread_spin_init(&spin,NULL);  /*初始化锁*/
     pthread_spin_lock(&spin);       /*获取锁*/
     pthread_spin_trylock(&spin);    
     pthread_spin_unlock(&spin);     /*释放锁*/

    对比:有的人说互斥锁是sleep-wait模式,自旋锁是busy-wait。我觉得很形象。

    互斥锁:锁被占用了?行吧,我也不是很急,我先小憩一下,并对调度器说:记得一会叫醒我。

    自旋锁:锁被占用了?我很急啊,快点快点啊,好了没啊,好了没啊…

    所以互斥锁用于可能阻塞很长时间的场景,自旋锁用于阻塞时间很短的场景。

    互斥锁的系统开销比自旋锁大得多,需要进行系统的调度,线程上下文切换等,如果阻塞时间很短,那代价就有点高了。

    读写锁

    读写锁其实是一种特殊的自旋锁。它把对共享资源的访问者分成了读者和写者,写是互斥的,一次只能有一个人写;读和写也是互斥的,有人写的时候就不能读;但读者之间不是互斥的,允许很多人一起读。

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

    这是一种更加实用的锁,提高了程序的并发性能。从实际应用角度出发,读锁的优先级应该要低于写锁的优先级。

    递归锁和非递归锁

    互斥锁按照是否可递归的性质划分的两种锁。递归锁又叫做可重入锁,指一个进程中可以多次进入锁;非递归锁又叫做不可重入锁。

    来看一个例子:

    MutexLock mutex;      
    void testa()  
    {  
        mutex.lock();  
        do_sth();
        mutex.unlock();  
    }     
    void testb()  
    {  
      mutex.lock();   
      testa();  
      mutex.unlock();   
    }

    如果mutex是非递归锁,那么调用testb()时就会造成死锁。如果mutex是递归锁,就允许这样多次进锁。

    linux中的锁默认为非递归锁,可以设置为递归锁。不过,不建议使用递归锁,程序容易出问题。

    条件变量

    提到锁还必须要介绍的是条件变量,通常条件变量和互斥锁搭配使用。

    条件变量让进程能够一直睡眠等待直到某种条件满足才开始执行。我们直接看最经典的例子,生产者和消费者的例子。我们有一个产品队列,假设是无穷大,消费者在队列空的时候不能消费,需要等待,代码如下:

    #include <pthread.h>
    struct msg {
    struct msg *m_next;
    /* ... more stuff here ... */
    };
    struct msg *workq;
    pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
    pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
    void process_msg(void)
    {
        struct msg *mp;
        for (;;) {
            pthread_mutex_lock(&qlock);
            while (workq == NULL)        /*a.*/
                pthread_cond_wait(&qready, &qlock);
            mp = workq;
            workq = mp->m_next;
            pthread_mutex_unlock(&qlock);
        }
    }
    void enqueue_msg(struct msg *mp)
    {
        pthread_mutex_lock(&qlock);
        mp->m_next = workq;
        workq = mp;
        pthread_mutex_unlock(&qlock);
        pthread_cond_signal(&qready);   /*b.*/
    }

    pthread_cond_wait()会先将锁释放,然后该进程进入阻塞。

    pthread_cond_signal()会通知wait的进程执行,结束阻塞。

    关于条件变量值得探讨的几个问题:

    1.有了互斥锁为什么还要条件变量?

    没有条件变量,只用互斥锁当然也可以解决问题。比如上面的例子,不用条件变量,因为互斥锁进入阻塞的进程不是一直阻塞的,是由调度器调度的,调度器不是根据是否有货决定唤醒的,而是根据调度策略。如果一直没有生产,那么消费者就会频繁被叫醒,醒来却发现什么也没有,又要去睡。这样会消耗很多资源。对于这种可能会阻塞很久的应用场景,使用条件变量,系统会在条件满足时才去唤醒进程,这样进程切换只发生一次,大大节省资源。所以条件变量是用于特殊场景的,为了提高资源利用率而产生的。

    2.在上面代码中注释a处,为什么使用while而不是if?

    这是一个很有意思的地方,也很巧妙。while和if不同之处在于,当wait结束时,仍然需要再循环一遍while条件,确保真的是有货可以消费了。如果是if,就会直接运行下面的代码。有时候可能会出现特殊情况,比如中断,故障,使得跳出了wait,这时再循环判断一次,程序的健壮性非常好。

    3.在代码注释b处,发送信号和解锁语句的前后顺序有什么要求呢?

    如果发送信号语句放在之前,被唤醒的时候锁还没有释放,是不是又会去sleep呢?在linux中操作系统的设计师考虑了这个问题,不会发生这样的情况。

    如果放在之后,会出现这样的情况:释放锁的瞬间,正好有另一个消费者进入消费,这时通知原来的消费者醒来,执行一次while判断,发现workq还是NULL,白醒来了。其实这样也是可以的,就是允许了消费者进程之间的竞争。这时这个while就很精髓了,如果是if,进程就不能知道它的货已经被抢了。

    不过在linux中一般让发送信号语句放在之前,不允许进程抢占原来已经等了很久的进程的货,这样也比较公平。

    *以上都是参照网络博客整理的,欢迎网友勘误,共同进步。

  • 相关阅读:
    [20180604]在内存修改数据(bbed).txt
    [20180630]truncate table的另类恢复2.txt
    [20180627]truncate table的另类恢复.txt
    [20180627]测试bbed是否支持管道命令.txt
    navicat连接异常 authentication plugin 'caching_sha2_password' 问题解决
    MYSQL类型与JAVA类型对应表
    Tomcat 8 Invalid character found in the request target. The valid characters are defined in RFC 3986
    springboot集成拦截器
    Caused by: java.lang.NoClassDefFoundError: org/apache/commons/pool2/impl/GenericObjectPoolConfig
    springboot集成过滤器
  • 原文地址:https://www.cnblogs.com/cpcpp/p/13391193.html
Copyright © 2020-2023  润新知