std::mutex和lock系列
1. std::mutex:独占的互斥量,不能递归使用。下面是它的类的部分定义:
class mutex { public: // std::mutex不支持拷贝和赋值操作。 mutex(const mutex&) = delete; mutex& operator=(const mutex&) = delete; constexpr mutex() noexcept; // 构造函数:新的对象是未锁的 ~mutex(); public: void lock(); // 上锁。会有三种情况 // (1) 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一直拥有该锁 // (2) 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住 // (3)如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。 void unlock(); // 解锁 bool try_lock(); // 尝试上锁。成功,返回true。失败时返回false,但不阻塞。会有三种情况 // (1) 如果当前互斥量没被其他线程占有,则锁住互斥量,直到该线程调用unlock // (2) 如果当前互斥量被其他线程占用,则调用线程返回false,且不会被阻塞 // (3) 如果互斥量己被当前线程锁住,则会产生死锁 };
为什么有些类会去禁止拷贝和赋值呢?主要是防止浅拷贝的问题,因为类中如果有指针的话,浅拷贝方式的结果是两个不同对象的指针
指向同一块内存区域,容易出现访问冲突,多次delete等错误,不是我们所希望的。
1)互斥量不允许拷贝,也不允许移动。新创建的互斥量对象是未上锁的。
2)lock和unlock必须成对出现,否则可能引起未定义行为。
注:实践中不推荐直接去调用成员函数lock(),调用lock()就意味着,必须在每个函数出口都要去调用unlock(),也包括异常的情况。
如果程序员没有进行unlock或者因为异常无法unlock,那么系统就会发生死锁。针对这个问题,C++11中引入了std::unique_lock
与std::lock_guard两种数据结构。通过对lock和unlock进行一次薄的封装(只是包装,真正的加锁和解锁还都是mutex完成的),
实现自动unlock的功能。
2. std::lock_guard:C++标准库为互斥量提供了一个RAII语法的模板类std::lock_guard,在构造时就能提供已锁的互斥量,并在析构的时候
进行解锁,从而保证了一个已锁互斥量能被正确解锁(自解锁),不会因为某个线程异常退出而影响其他线程。下面是它的类的部分定义。
struct adopt_lock_t {}; // 空的标记类 constexpr adopt_lock_t adopt_lock {}; // 常量对象 template <class _Mutex> class lock_guard { public: using mutex_type = _Mutex; explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { _MyMutex.lock(); } // 构造,并加锁 lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} // 只构造,不加锁 ~lock_guard() noexcept { _MyMutex.unlock(); } // unlock lock_guard(const lock_guard&) = delete; lock_guard& operator=(const lock_guard&) = delete; private: _Mutex& _MyMutex; };
1)lock_guard对象不可拷贝和移动。
2)它有两个重载的构造函数,其中lock_gurad(_Mutex&)会自动对_Mutex进行加锁,而lock_gurad(_Mutex&,adopt_lock_t)则只构造
但不加锁,因此需要在某个时候通过调用_Mutex本身的lock()进行上锁。(说明:adopt_lock_t是个空的标签类,起到通过标签来重载构造函数的作用)。
3)在lock_gurad对象的生命周期内,它所管理的Mutex对象会一直保持上锁状态,直至生命周期结束后才被解锁。不需要,也无法手动通过lock_gurad对
Mutex进行上锁和解锁操作。从总体上而言,没有给程序员提供足够的灵活度来对互斥量的进行上锁和解锁控制。
3. std::unique_lock:std::unique_lock 与std::lock_guard都能实现自动加锁与解锁功能,但是std::unique_lock要比std::lock_guard更
灵活,但是更灵活的代价是占用空间相对更大一点且相对更慢一点。下面是它的类的部分定义:
// 空的标记类 struct adopt_lock_t {}; struct defer_lock_t {}; struct try_to_lock_t {}; // 常量对象 constexpr adopt_lock_t adopt_lock {}; constexpr defer_lock_t defer_lock {}; constexpr try_to_lock_t try_to_lock {}; template <class _Mutex> class unique_lock { // 在析构函数中自动解锁mutex public: using mutex_type = _Mutex; unique_lock() noexcept : _Pmtx(nullptr), _Owns(false) { // 默认构造函数 } explicit unique_lock(_Mutex& _Mtx) : _Pmtx(_STD addressof(_Mtx)), _Owns(false) { // 构造并上锁。 _Pmtx->lock(); // 如果其他unique_lock己拥有该_Mtx,则会阻塞等待 _Owns = true; // 成功获取锁,拥有锁的所有权。 } unique_lock(_Mutex& _Mtx, adopt_lock_t) : _Pmtx(_STD addressof(_Mtx)), _Owns(true) { // 构造,并假定己上锁(mutex需要在外面事先被锁住)。注意拥有锁的所有权 } unique_lock(_Mutex& _Mtx, defer_lock_t) noexcept : _Pmtx(_STD addressof(_Mtx)), _Owns(false) { // 构造,但不上锁。false表示并未取得锁的所有权。 } unique_lock(_Mutex& _Mtx, try_to_lock_t) : _Pmtx(_STD addressof(_Mtx)), _Owns(_Pmtx->try_lock()) { // 构造,并尝试上锁。如果上锁不成功,并不会阻塞当前线程 } //支持移动构造 unique_lock(unique_lock&& _Other) noexcept : _Pmtx(_Other._Pmtx), _Owns(_Other._Owns) { // 移动拷贝,destructive copy _Other._Pmtx = nullptr; // 失去对原mutex的所有权 _Other._Owns = false; } //支持移动赋值 unique_lock& operator=(unique_lock&& _Other) { // 移动赋值, destructive copy if (this != _STD addressof(_Other)) { // different, move contents if (_Owns) { _Pmtx->unlock(); } _Pmtx = _Other._Pmtx; _Owns = _Other._Owns; _Other._Pmtx = nullptr; _Other._Owns = false; } return *this; } ~unique_lock() noexcept { // clean up if (_Owns) { _Pmtx->unlock(); // 析构函数中解锁 } } unique_lock(const unique_lock&) = delete; unique_lock& operator=(const unique_lock&) = delete; void lock() { // lock the mutex _Validate(); _Pmtx->lock(); _Owns = true; } _NODISCARD bool try_lock() { // try to lock the mutex _Validate(); _Owns = _Pmtx->try_lock(); return _Owns; } void unlock() { // try to unlock the mutex if (!_Pmtx || !_Owns) { _THROW(system_error(_STD make_error_code(errc::operation_not_permitted))); } _Pmtx->unlock(); _Owns = false; } void swap(unique_lock& _Other) noexcept { // swap with _Other _STD swap(_Pmtx, _Other._Pmtx); _STD swap(_Owns, _Other._Owns); } _Mutex* release() noexcept { // 返回指向它所管理的 Mutex 对象的指针,并释放所有权 _Mutex* _Res = _Pmtx; _Pmtx = nullptr; _Owns = false; return _Res; } _NODISCARD bool owns_lock() const noexcept { return _Owns; } // 返回当前 std::unique_lock 对象是否获得了锁 explicit operator bool() const noexcept { return _Owns; } // 返回当前 std::unique_lock 对象是否获得了锁 _NODISCARD _Mutex* mutex() const noexcept { return _Pmtx; } // return pointer to managed mutex private: _Mutex* _Pmtx; bool _Owns; // 是否拥有锁(当mutex被lock时,为true;否则为false) };
1)以独占所有权的方式管理Mutex对象的上锁和解锁操作,即没有其他的unique_lock对象同时拥有某个Mutex对象的所有权。
2)与std::lock_guard一样,在unique_lock生命期结束后,会对其所管理的Mutex进行解锁。(注意:unique_lock只对拥有所有权的mutex才会在析构函数中被自动unlock)。
3)这里再介绍下unique_lock的构造函数:
a. unique_lock()默认构造函数:新创建的unique_lock对象不管理任何Mutex对象。
b. unique_lock(_Mutex& m):构造并上锁。如果此时某个另外的unique_lock己管理m对象,则当前线程会被阻塞。
c. unique_lock(_Mutex& m, adopt_lock_t):构造,并假定m己上锁。(m需要事先被上锁,构造结束后unique_lock就拥有m的所有权)
d. unique_lock(_Mutex& _Mtx, defer_lock_t):构造,但不上锁。对象创建以后,可以手动调用unique_lock的lock来上锁,才拥有_Mtx的所有权。
强调一下,只有拥有所有权的mutex才会在析构函数中被自动unlock。
e. unique_lock(_Mutex& _Mtx, try_to_lock_t):构造,并尝试上锁。如果上锁不成功,并不会阻塞当前线程。