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

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

原文地址:https://www.cnblogs.com/cpcpp/p/13391193.html