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

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

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


锁lock的类型

lock_guard C++11 区域锁
unique_lock C++11 区域锁,提供了更加灵活的操作
shared_lock C++14 提供共享的互斥量的锁操作
scoped_lock C++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 {	            // 若对象持有锁,则返回true
		return (_Owns);
	}

	explicit operator bool() const _NOEXCEPT {	    // 若对象持有锁,则返回true
		return (_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()方法进行加锁。


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

猜你喜欢

转载自blog.csdn.net/hujingshuang/article/details/70243025
今日推荐