《探索C++多线程》:condition_variable源码(二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hujingshuang/article/details/70792957

        在上一文章《探索C++多线程》:condition_variable源码(一)中,学习到了condition_variable的原理和一些使用方法,其实在头文件<condition_variable>中还有另外一类条件变量:condition_variable_any,也就是这篇文章要讲到的了。

        从名称上看,condition_variable与condition_variable_any的差别就是any,那么这个any是怎样体现出来的呢?C++11给出了解答:


        如果没有看明白,那么再来:


        现在应该比较明了了吧,说白了就是:condition_variable对象的方法只能用unique_lock<mutex>类型的锁,而condition_variable_any的方法能使用任何类型的锁,除此之外,二者并无大的差别。

        

其实,在这篇文章中,我想解决两个问题:

        问题一:为什么condition_variable要配合使用 锁 而不是 互斥量 呢?

        答案:这么做的目的就是为了保证多线程的异常安全。

试想,有这么一个线程:

mutex mtx;

void foo() {
    mtx.lock();
    while (not_ready) {
		// ...
        cv.wait(mtx);
	}
    mtx.unlock();
}

        若该线程对信号量mtx上锁之后,就会继续往下执行,如果说在执行mtx.unlock()之前,发生了安全异常,那么该线程就无法执行解锁了,也就是说互斥量一直被上锁状态,而使得其他线程无法获得持有权。如果,想要在异常情况下保证线程安全,那么需要程序员在编写代码上将异常情况考虑进去(使用try/catch/throw等等),一旦发生异常便要进行解锁操作,这样徒劳增编程的复杂度和代码量。

        那么,由于有lock_guard与unique_lock这两个利器,现在就比较好办了,所以现在代码改为:

mutex mtx;

void foo() {
    unique_lock<mutex> lck(mtx);
    while (not_ready) {
		// ...
        cv.wait(mtx);
	}
}

        这样一来,代码也变得简洁了许多。不论程序员是忘记了解锁,还是线程发生了异常,unique_lock的析构函数都会自动解锁,能够保证线程的异常安全。


        问题二:为什么condition_variable的 锁类型 不是 lock_guard 而是 unique_lock 呢?

        答案:这不仅是为了安全考虑,更主要的是只有unique_lock才能做到

我们试想这么一个例子:

mutex mtx;

void foo() {
    lock_guard<mutex> lck(mtx);
    while (not_ready) {
		// ...
        cv.wait(mtx);
	}
}
        跟unique_lock类似。若该线程持有了锁,那么将会继续执行后面的语句,我们现在来分析一下关于互斥量上锁的情况:lock_guard加锁、wait解锁、wait加锁、lock_guard析构解锁。

        那么问题就来了:

                1、由于lock_guard只能在构造函数中对互斥量mtx加锁,且在析构函数中将互斥量mtx解锁,那么这个互斥量在wait中是怎么加锁、解锁的呢?这就根本木有办法,所以lock_guard是行不通滴;

                2、退一万步港,就算就算lock_guard能够在wait中上锁,解锁。如果wait解锁到wait加锁这期间发生了异常,那么此时互斥量是处于解锁的状态,而异常发生后局部变量lock_guard要析构,在析构的时候就会进行解锁,此时解锁就会出错,因为mtx身上已经没有锁了。也就是说lock_guard确实是解决了一方面的异常安全,但也带来了另外的异常安全。

        我们倒回来,再想一下unique_lock为什么可以做到异常安全呢?

        如果忘记了unique_lock的源码,建议回顾一下:《探索C++多线程》:mutex源码(二)。在这里我还是把它粘出来,unique_lock的析构函数:

~unique_lock() _NOEXCEPT { 	// 析构  
	if (_Owns)				// 若互斥量在析构之前已经解锁了,由于存在_Owns标志,不再解锁,所以不会发生析构异常  
		_Pmtx->unlock();	// 若没有,则正常析构  
}
        再来对比看一下lock_guard的析构函数:

~lock_guard() _NOEXCEPT {	// 析构函数,解锁
	_MyMutex.unlock();
}
        这样就明了一些了吧。另外,由于unique_lock提供了更多灵活的方法供我们使用,而lock_guard不提供任何方法,这也是一个因素,当然最重要的还是安全啦。
        大家可以参考:

                1、《Effective C++》条款29:为“异常安全”而努力是值得的;

                2、StackOverflow:C++11: why does std::condition_variable use std::unique_lock?

猜你喜欢

转载自blog.csdn.net/hujingshuang/article/details/70792957