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

来源:互联网 发布:jquery.mloading.js 编辑:程序博客网 时间:2024/06/06 05:03

        通过前面三节,我们学习了线程、互斥量,乘热打铁,在这一节中我们来学习关于锁(lock)的相关知识,关于锁的声明和定义,也在头文件<mutex>中。


锁lock的类型

lock_guardC++11区域锁unique_lockC++11区域锁,提供了更加灵活的操作shared_lockC++14提供共享的互斥量的锁操作scoped_lockC++17提供多个互斥量时避免死锁RAII的锁操作

        很多时候我们在编写多线程代码时,在对互斥量加锁lock()后,有时可能会忘记解锁unlock(),这样就会导致该线程一直持有互斥量,而其他与其存在资源竞争的线程将会一直获取不到所有权。这完全就违背我我们利用多线程处理任务愿望。


关于lock_guard

        在了解lock_guard之前,我们先来看一段代码:

#include <iostream>#include <thread>#include <mutex>using namespace std;volatile int cnt = 0;mutex mtx;void task() {    for (int i = 0; i < 10; i++) {        lock_guard<mutex> lck(mtx);     // 使用lock_guard        cout << this_thread::get_id() << "-----" << cnt++ << endl;    }}int main() {    thread t[3];    for (int i = 0; i < 3; i++) {        t[i] = thread(task);    }    for (int i = 0; i < 3; i++) {        t[i].join();    }    return 0;}
        看完上面的代码,可能大家会有疑问,虽然有互斥量mtx,但是并没有调用mtx对象方法:mtx.lock()或者mtx.try_lock(),也没有 出现mtx.unlock()啊。程序是怎么做到加锁、解锁的呢?

        其实这就是 lock_guard<mutex> lck(mtx) 这句代码起到的功效了。好的,现在我们带着疑问来解读一下lock_guard了。

其定义如下:

template<class _Mutex>class lock_guard {            // 利用析构函数解锁互斥量public:typedef _Mutex mutex_type;explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) {    // ① 构造,并加锁_MyMutex.lock();}lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) { } // ② 构造,但不加锁~lock_guard() _NOEXCEPT {// 析构函数,解锁_MyMutex.unlock();}lock_guard(const lock_guard&) = delete;lock_guard& operator=(const lock_guard&) = delete;private:_Mutex& _MyMutex;};
        由上述定义可以知道,这是一个类模板,结构很简单,该类中定义了一个引用变量,还提供了构造函数、析构函数,禁用了拷贝构造函数和赋值函数。

lock_guard<mutex> lck(mtx);
        我们再来看上面这句代码做了些什么事?显然使用的构造函数①,在初始化列表中初始化了互斥量,在函数体中对互斥量进行上锁,因此实例化一个对象的时候,便已经上锁了。

        那么什么时候解锁呢?我们看到在析构函数中,对互斥量进行了解锁。也就是说,如果要解锁,那么就必须调用析构函数;我们回到上面之前的代码中分析,在for循环中,每结束一次循环,便会自动将lck析构掉,因此lock_guard的工作原理便很清晰明了了。

        总的来说:lock_guard本质就是,利用程序块结束,对象被析构时进行自动解锁。这样一来,我们只要将需要进行加锁的操作(设计资源共享的代码部分)与实例化lock_guard对象放在一个语句块中即可。如:

volatile int cnt = 0;mutex mtx;void task() {    for (int i = 0; i < 1000; i++) {        {   // begin            lock_guard<mutex> lck(mtx);            cnt++;            if (cnt > 1000)     cnt = 1000;            else if (cnt < 0)   cnt = 0;        }   // end    }}
        另外,lock_guard类中还有一个方法,在源码的注释中讲到了,该方法也是实例化一个对象,但不对互斥量上锁。主要是为了避免互斥量在已经被上锁的情况下再上锁,用法如下:

volatile int cnt = 0;mutex mtx;void task() {    for (int i = 0; i < 1000; i++) {        {   // begin            mtx.lock();        // 已上锁            lock_guard<mutex> lck(mtx, adopt_lock);            cnt++;            if (cnt > 1000)     cnt = 1000;            else if (cnt < 0)   cnt = 0;        }   // end    }}
        其中adopt_lock是一个空类(结构体),相信读过侯JJ《STL源码剖析》的都知道(用一个空类或结构体来进行方法的重载的用法),这里也是同样的用法。
        其声明与定义如下:

struct adopt_lock_t { };struct defer_lock_t { };struct try_to_lock_t { };extern _CRTIMP2_PURE const adopt_lock_t adopt_lock;extern _CRTIMP2_PURE const defer_lock_t defer_lock;extern _CRTIMP2_PURE const try_to_lock_t try_to_lock;
        好了,以上就是对lock_guard的解析、用途和使用用法。


关于unique_lock

        lock_guard锁非常的简单和方便,但它实在是太简洁了,而unique_lock锁为程序员提供更为灵活、强大的互斥操作、管理方法,既然是要强大一下,自然其定义就会复杂,更长一些,如下:

template<class _Mutex>class unique_lock {public:typedef unique_lock<_Mutex> _Myt;typedef _Mutex mutex_type;    // --------------------------------------构造,赋值,销毁---------------------------------------------unique_lock() _NOEXCEPT : _Pmtx(0), _Owns(false) { }                                        // ① 构造,空explicit unique_lock(_Mutex& _Mtx) : _Pmtx(&_Mtx), _Owns(false) {                        // ② 构造,并上锁_Pmtx->lock();_Owns = true;}unique_lock(_Mutex& _Mtx, adopt_lock_t) : _Pmtx(&_Mtx), _Owns(true) { }                    // ③ 构造,假设已经上锁unique_lock(_Mutex& _Mtx, defer_lock_t) _NOEXCEPT : _Pmtx(&_Mtx), _Owns(false) { }        // ④ 构造,不上锁unique_lock(_Mutex& _Mtx, try_to_lock_t) : _Pmtx(&_Mtx), _Owns(_Pmtx->try_lock()) { }    // ⑤ 构造,尝试上锁    // ⑥ 构造,在_Rel_time时间内尝试上锁template<class _Rep, class _Period>unique_lock(_Mutex& _Mtx, const chrono::duration<_Rep, _Period>& _Rel_time) : _Pmtx(&_Mtx), _Owns(_Pmtx->try_lock_for(_Rel_time)) { }// ⑦ 构造,在_Rel_time时间内尝试上锁template<class _Clock, class _Duration>unique_lock(_Mutex& _Mtx, const chrono::time_point<_Clock, _Duration>& _Abs_time) : _Pmtx(&_Mtx), _Owns(_Pmtx->try_lock_until(_Abs_time)) { }unique_lock(_Mutex& _Mtx, const xtime *_Abs_time) : _Pmtx(&_Mtx), _Owns(false) {        // ⑧ 构造,在_Abs_time时间点到来之前尝试上锁_Owns = _Pmtx->try_lock_until(_Abs_time);}unique_lock(unique_lock&& _Other) _NOEXCEPT : _Pmtx(_Other._Pmtx), _Owns(_Other._Owns) {// ⑨ 构造,使用move赋值_Other._Pmtx = 0;_Other._Owns = false;}    // ----------------------------------------加锁,解锁-------------------------------------------unique_lock& operator=(unique_lock&& _Other) _NOEXCEPT {// 赋值函数,使用move赋值if (this != &_Other) {                    // _Other与自身对象不同(即:不是自己赋值给自己)if (_Owns)                              // 已锁定_Pmtx->unlock();                    // 解锁_Pmtx = _Other._Pmtx;_Owns = _Other._Owns;_Other._Pmtx = 0;_Other._Owns = false;}return (*this);}~unique_lock() _NOEXCEPT {                    // 析构if (_Owns)                            // 若互斥量在析构之前已经解锁了,由于存在_Owns标志,所以不会发生析构异常_Pmtx->unlock();                    // 若没有,则正常析构}unique_lock(const unique_lock&) = delete;                   // 禁用拷贝构造函数unique_lock& operator=(const unique_lock&) = delete;        // 禁用复制函数void lock() {                                // 上锁_Pmtx->lock();_Owns = true;}bool try_lock() _NOEXCEPT {                    // 尝试上锁_Owns = _Pmtx->try_lock();return (_Owns);}template<class _Rep, class _Period>bool try_lock_for(const chrono::duration<_Rep, _Period>& _Rel_time) {        // 在一段时间内(_Rel_time)尝试上锁_Owns = _Pmtx->try_lock_for(_Rel_time);return (_Owns);}template<class _Clock, class _Duration>bool try_lock_until(const chrono::time_point<_Clock, _Duration>& _Abs_time) {// 尝试上锁,直到绝对时间点(_Abs_time)到来_Owns = _Pmtx->try_lock_until(_Abs_time);return (_Owns);}bool try_lock_until(const xtime *_Abs_time) {   // 尝试上锁,直到绝对时间点(_Abs_time)到来_Owns = _Pmtx->try_lock_until(_Abs_time);return (_Owns);}void unlock() {                                 // 解锁_Pmtx->unlock();_Owns = false;}    // ----------------------------------------变换-------------------------------------------void swap(unique_lock& _Other) _NOEXCEPT {    //交换量unique_lock_STD swap(_Pmtx, _Other._Pmtx);_STD swap(_Owns, _Other._Owns);}_Mutex *release() _NOEXCEPT {                // 断开与对象的关联(注意,调用该方法后互斥量并未解锁,所以要记得在合适的时候解锁哦)_Mutex *_Res = _Pmtx;_Pmtx = 0;_Owns = false;return (_Res);                          //返回unique_lock对象管理的互斥量对象的指针(下一步要解锁的话,可以用_Res->unlock())}    // ----------------------------------------查看-------------------------------------------bool owns_lock() const _NOEXCEPT {            // 若对象持有锁,则返回truereturn (_Owns);}explicit operator bool() const _NOEXCEPT {    // 若对象持有锁,则返回truereturn (_Owns);}_Mutex *mutex() const _NOEXCEPT {            // 返回互斥量的指针return (_Pmtx);}private:_Mutex *_Pmtx;      // 互斥量bool _Owns;         // 上锁标识};

        代码是有一点点长,光是构造函数就有9个(⊙o⊙)…闷一口气,我们接下来动手开始分析。

        先来看是私有成员变量:一个是互斥量_Mutex,一个是标志_Owns(此标志用来表示互斥量有没有被上锁:若上锁,则为true;否则,为false)。

        构造函数

                ① 构造一个空的unique_lock对象;

                ② 使用互斥量构造unique_lock对象,并对互斥量上锁(_Owns = ture);

                ③ 使用已上锁的互斥量构造unique_lock对象(_Owns = ture);

                ④ 使用互斥量构造unique_lock对象,但不对互斥量上锁(_Owns = false);

                ⑤ 使用互斥量构造unique_lock对象,并尝试对互斥量上锁(_Owns = _Pmtx->try_lock());

                ⑥ 使用互斥量构造unique_lock对象,并在_Rel_time时间内尝试对互斥量上锁(_Owns = _Pmtx->try_lock_for(_Rel_time));

                ⑦ 使用互斥量构造unique_lock对象,并在_Abs_time时间内尝试对互斥量上锁(_Owns = _Pmtx->try_lock_until(_Abs_time));

                ⑧ 使用互斥量构造unique_lock对象,并在_Abs_time时间点到来之前尝试对互斥量上锁(_Owns = _Pmtx->try_lock_until(_Abs_time));

                ⑨ 使用通过move(_Mutex)得到的互斥量构造unique_lock对象;

        通过构造函数,我们有了大致的了解。接下来我们继续分析unique_lock是如何加上锁与解锁的。


加锁与解锁

        加锁:构造函数、显式的调用lock()、try_lock()、try_lock_for()、try_lock_until()方法,这样就可以对互斥量进行加锁了;

        解锁:显式的调用析构函数、调用unlock()方法;


unique_lock的变换

        1、swap()    —— 交换量unique_lock:_Pmtx,_Owns都需要交换(如定义);

        2、release() —— 断开unique_lock对象与互斥量的关联(返回unique_lock管理的互斥量的指针,将_Owns 置0,注意此时并未解锁);


其他方法

        1、unique_lock::owns_lock():判断unique_lock对象是否持有锁;

        2、unique_lock::operator:判断unique_lock对象是否持有锁;

        3、unique_lock::mutex():返回unique_lock对象管理的互斥量的指针;

以上便是unique_lock的主要特性,灵活使用unique_lock会给程序上带来很大的便利性。


补充

        最后再补充一下之前漏掉的几个空类(结构体)的意义,这里再摘抄一遍:

struct adopt_lock_t { };struct defer_lock_t { };struct try_to_lock_t { };extern _CRTIMP2_PURE const adopt_lock_t adopt_lock;extern _CRTIMP2_PURE const defer_lock_t defer_lock;extern _CRTIMP2_PURE const try_to_lock_t try_to_lock;

       adopt_lock_t(对应adopt_lock)

                作为lock_guard或unique_lock构造参数的值。adopt_lock作为参数构造锁(lock_guard或unique_lock)对象时,假设互斥量已经由当前线程加锁了,所以不会对互斥量进行上锁。

        defer_lock_t(对应defer_lock)

                作为lock_guard或unique_lock构造参数的值。defer_lock作为参数构造锁(lock_guard或unique_lock)对象时,不加锁也不持有锁。

        try_to_lock_t(对应try_to_lock)

                作为lock_guard或unique_lock构造参数的值。try_to_lock作为参数构造锁(lock_guard或unique_lock)对象时,会调用互斥量的try_lock()方法进行加锁。


        若有理解失误或表达不清的地方,还请大家多多指教,谢谢~

1 0