第29课 互斥量与自解锁(std::mutex和lock系列)
Posted 5iedu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第29课 互斥量与自解锁(std::mutex和lock系列)相关的知识,希望对你有一定的参考价值。
一. 互斥量
(一)Mutex系列类
1. std::mutex:独占的互斥量,不能递归使用。
2. std::recursive_mutex:递归互斥量。允许同一线程多次获得该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。
3. std::time_mutex和std::recursive_time_mutex:带超时的互斥量。前者是超时的独占锁,后者为超时的递归锁。主要用于获取锁时增加超时等待功能,因为有时不知道获取锁需要多久,为了不至于一直等待下去,就设置一个等待超时时间。比std::mutex多了两个超时获取锁的接口:try_lock_for和try_lock_until
//1. 独占互斥锁,不能递归使用 class mutex { public: //std::mutex不支持copy和move操作。 mutex(const mutex&) = delete; mutex& operator=(const mutex&) = delete; constexpr mutex() noexcept; //构造函数:新的对象是未锁的 ~mutex(); void lock(); //上锁 void unlock(); //解锁 bool try_lock(); //尝试上锁。成功,返回true。失败时返回false,但不阻塞。会有三种情况 //(1)如果当前互斥量没被其他线程占有,则锁住互斥量,直到该线程调用unlock //(2)如果当前互斥量被其他线程占用,则调用线程返回false,且不会被阻塞 //(3)如果互斥量己被当前线程锁住,则会产生死锁 }; //2. 递归互斥锁可以被同一个线程多次加锁(增加锁计数),以获得对互斥锁对象的多层所有权。 //可以用来解决同一线程需要多次获取互斥量时死锁的问题 class recursive_mutex { public : recursive_mutex(const recursive_mutex&) = delete; recursive_mutex& operator=(const recursive_mutex&) = delete; recursive_mutex() noexcept; ~recursive_mutex(); void lock(); void unlock(); //释放互斥量时需要调用与该锁层次深度相同次数的unlock(),即lock()次数和unlock()次数相同 bool try_lock() noexcept; }; //3. 带超时的互斥锁,不能递归使用 class timed_mutex { public : timed_mutex(const timed_mutex&) = delete; timed_mutex& operator=(const timed_mutex&) = delete; timed_mutex(); ~timed_mutex(); void lock(); void unlock(); bool try_lock(); //在指定的relative_time时间内,尝试获取*this上的锁。当relative_time.count()<=0时 //将立即返回,就像调用try_lock()样。否则会阻塞,直到获取锁或超过给定的relative_time时间 //当锁被调用线程获取,返回true;反之,返回false. template<typename Rep, typename Period> bool try_lock_for(const std::chrono::duration<Rep, Period>& relative_time); //在指定的absolute_time时间内,尝试获取*this上的锁。当absolute_time<=clock::now()时 //将立即返回,就像调用try_lock()一样。否则会阻塞,直到获取锁或Clock::now()返回的 //时间等于或超过给定的absolute_time的时间。返回值含义与try_lock_for相同。 template<typename Clock, typename Duration> bool try_lock_until(const std::chrono::time_point<Clock, Duration>& absolute_time); }; //4. 带定时的递归互斥锁 class recursive_timed_mutex { public : recursive_timed_mutex(const recursive_timed_mutex&) = delete; recursive_timed_mutex& operator=(const recursive_timed_mutex&) = delete; recursive_timed_mutex(); ~recursive_timed_mutex(); void lock(); void unlock(); bool try_lock() noexcept; template<typename Rep, typename Period> bool try_lock_for(const std::chrono::duration<Rep, Period>& relative_time); template<typename Clock, typename Duration> bool try_lock_until(const std::chrono::time_point<Clock, Duration>& absolute_time); };
(二)注意事项
1. lock和unlock必须成对出现,否则可能引起未定义行为。为了防止出现这种行为,可用std::lock_guard、std::unique_lock等RAII方式来使用mutex。
2. 尽量不要使用递归锁,主要原因如下:
(1)需要用到递归锁定的多线程互斥处理往往本身就可以简化,允许递归互斥很容易放纵复杂逻辑的产生,从而导致一些多线程同步引起的晦涩问题。
(2)递归锁比起非递归锁,效率会更低一些。
(3)递归锁虽然允许同一线程多次获得同一个互斥量,可重复获得最大次数并未具体说明,一旦超过一定次数,再对lock进行调用就会抛出std::system错误。
3. 互斥量不允许拷贝,也不允许移动。新创建的互斥量对象是未上锁的。
4. 多线程中,对多个互斥量须遵循同样的顺序进行加锁,否则可能会产生死锁现象。
【编程实验】mutex系列
#include <iostream> #include <mutex> #include <thread> #include <chrono> #include <list> //1. 独占互斥量 std::mutex g_mtx; int tickets = 100; //1.1 多线程模拟火车站多窗口售票 void sell_ticket() { while (true) { g_mtx.lock(); if (tickets > 0) { std::cout << "thread id(" << std::this_thread::get_id() << ") sell ticket: " << 101 - tickets-- << std::endl; } else { g_mtx.unlock(); break; } g_mtx.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } } //1.2 消息处理系统 class MsgManage { std::mutex mtx; std::list<int> lst; public: MsgManage(){} ~MsgManage() {} void InMsg() { for (int i = 0; i < 100; ++i) { mtx.lock(); std::cout << "insert element: " << i << std::endl; lst.push_back(i); mtx.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(3)); } } void outMsg() { while (true) { int num = 0; mtx.lock(); if (!lst.empty()) { num = lst.front(); lst.pop_front(); std::cout << "remove element: " << num << std::endl; }else { std::cout << "message queue is empty!" << std::endl; } if (num == 99) { mtx.unlock(); break;} mtx.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(4)); } } }; //2. 递归互斥量 struct Complex { std::recursive_mutex rmtx; int i; Complex(int i):i(i){} void mul(int x) { rmtx.lock(); i *= x; rmtx.unlock(); } void div(int x) { rmtx.lock(); i /= x; rmtx.unlock(); } void both(int x, int y) { rmtx.lock(); mul(x); //递归,会使得同一线程两次获取互斥量 div(y); rmtx.unlock(); } }; //3. 带有超时的互斥量 std::timed_mutex tmutex; void work() { std::chrono::microseconds timeout(100); while (true) { if (tmutex.try_lock_for(timeout)) { std::cout << std::this_thread::get_id() << ": do work with this mutex" << std::endl; std::chrono::microseconds sleepDuration(250); std::this_thread::sleep_for(sleepDuration); tmutex.unlock(); }else { std::cout << std::this_thread::get_id() << ": do work without mutex" << std::endl; std::chrono::microseconds sleepDuration(100); std::this_thread::sleep_for(sleepDuration); } } } using namespace std; int main() { //1. 独占互斥量 std::thread ths[10]; for (int i = 0; i < 10; ++i) { ths[i] = std::thread(sell_ticket); } for (auto& th : ths) { th.join(); } MsgManage manage; std::thread outMsg(&MsgManage::outMsg, &manage); std::thread inMsg(&MsgManage::InMsg, &manage); inMsg.join(); outMsg.join(); //2. 递归互斥量 Complex cmpl(1); cmpl.both(32, 23); //3. 带超时的互斥量 std::thread t1(work); std::thread t2(work); t1.join(); t2.join(); return 0; }
二. 自解锁
(一)std::lock_guard
//空的标记类 struct adopt_lock_t {}; //常量对象 constexpr adopt_lock_t adopt_lock {}; // CLASS TEMPLATE lock_guard 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 { // unlock _MyMutex.unlock(); } lock_guard(const lock_guard&) = delete; lock_guard& operator=(const lock_guard&) = delete; private: _Mutex& _MyMutex; };
1. 利用RAII技术在析构函数中对Mutex自动解锁。但要注意,lock_guard并不负责管理Mutex的生命期。在lock_gurad对象的生命周期内,它所管理的Mutex对象会一直保持上锁状态,直至生命周期结束后才被解锁。不需要,也无法通过手动通过lock_gurad对Mutex进行上锁和解锁操作。从总体上而言,没有给程序员提供足够的灵活度来对互斥量的进行上锁和解锁控制。
2. 它有两个重载的构造函数,其中lock_gurad(_Mutex&)会自动对_Mutex进行加锁,而lock_gurad(_Mutex&,adopt_lock_t)则只构造但不加锁,因此需要在某个时候通过调用_Mutex本身的lock()进行上锁 (说明:adopt_lock_t是个空的标签类,起到通过标签来重载构造函数的作用)。
3.模板参数表示互斥量类型。如std::mutex、std::recursive_mutex、std::timed_mutex,std::recursive_timed_mutex等。这些互斥量只有几种基本操作:lock和unlock,以及try_lock系列函数。
4. lock_guard对象不可拷贝和移动。
(二)std::unique_lock
//空的标记类 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 {}; // CLASS TEMPLATE unique_lock template <class _Mutex> class unique_lock { // 在析构函数中自动解锁mutex public: using mutex_type = _Mutex; // CONSTRUCT, ASSIGN, AND DESTROY 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()) { // 构造,并尝试上锁。如果上锁不成功,并不会阻塞当前线程 } template <class _Rep, class _Period> unique_lock(_Mutex& _Mtx, const chrono::duration<_Rep, _Period>& _Rel_time) //在给定的时长内尝试获取锁。 : _Pmtx(_STD addressof(_Mtx)), _Owns(_Pmtx->try_lock_for(_Rel_time)) { // construct and lock with timeout } template <class _Clock, class _Duration> unique_lock(_Mutex& _Mtx, const chrono::time_point<_Clock, _Duration>& _Abs_time) //在给定的时间点内尝试获取锁 : _Pmtx(_STD addressof(_Mtx)), _Owns(_Pmtx->try_lock_until(_Abs_time)) { // construct and lock with timeout } unique_lock(_Mutex& _Mtx, const xtime* _Abs_time) : _Pmtx(_STD addressof(_Mtx)), _Owns(false) { // try to lock until _Abs_time _Owns = _Pmtx->try_lock_until(_Abs_time); } //支持移动构造 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; } template <class _Rep, class _Period> _NODISCARD bool try_lock_for(const chrono::duration<_Rep, _Period>& _Rel_time) { // try to lock mutex for _Rel_time _Validate(); _Owns = _Pmtx->try_lock_for(_Rel_time); return _Owns; } template <class _Clock, class _Duration> _NODISCARD bool try_lock_until( const chrono::time_point<_Clock, _Duration>& _Abs_time) { // try to lock mutex until _Abs_time _Validate(); _Owns = _Pmtx->try_lock_until(_Abs_time); return _Owns; } _NODISCARD bool try_lock_until(const xtime* _Abs_time) { // try to lock the mutex until _Abs_time _Validate(); _Owns = _Pmtx->try_lock_until(_Abs_time); 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 { // 返回当前 std::unique_lock 对象是否获得了锁 return _Owns; } explicit operator bool() const noexcept { // 返回当前 std::unique_lock 对象是否获得了锁 return _Owns; } _NODISCARD _Mutex* mutex() const noexcept { // return pointer to managed mutex return _Pmtx; } private: _Mutex* _Pmtx; bool _Owns; //是否拥有锁(当mutex被lock时,为true;否则为false) void _Validate() const { // check if the mutex can be locked if (!_Pmtx) { _THROW(system_error(_STD make_error_code(errc::operation_not_permitted))); } if (_Owns) { _THROW(system_error(_STD make_error_code(errc::resource_deadlock_would_occur))); } } };
1. 以独占所有权的方式管理Mutex对象的上锁和解锁操作,即没有其他的unique_lock对象同时拥有某个Mutex对象的所有权。同时要注意,unique_lock并不负责管理Mutex的生命期。
2. 与std::lock_guard一样,在unique_lock生命期结束后,会对其所管理的Mute进行解锁(注意:unique_lock只对拥有所有权的mutex才会在析构函数中被自动unlock)。但unique_lock比lock_guard使用更加灵活,功能更加强大
3. unique_lock模板参数的类型与lock_guard类型的含义相同。
4. unique_lock的构造函数:
(1)unique_lock()默认构造函数:新创建的unique_lock对象不管理任何Mute对象。
(2)unique_lock(_Mutex& m):构造并上锁。如果此时某个另外的unique_lock己管理m对象,则当前线程会被阻塞。
(3)unique_lock(_Mutex& m, adopt_lock_t):构造,并假定m己上锁。(m需要事先被上锁,构造结束后unique_lock就拥有m的所有权)
(4)unique_lock(_Mutex& _Mtx, defer_lock_t):构造,但不上锁。对象创建以后,可以手动调用unique_lock的lock来上锁,才拥有_Mtx的所有权(再次强调一下,只有拥有所有权的mutex才会在析构函数中被自动unlock)
(5)unique_lock(_Mutex& _Mtx, try_to_lock_t):构造,并尝试上锁。如果上锁不成功,并不会阻塞当前线程。
(6)在给定时长内或给定时间点尝试获锁,并构造unique_lock对象。
5.上锁/解锁操作:lock,try_lock,try_lock_for,try_lock_until 和 unlock
(三)std::lock和std::scoped_lock
1. void std::lock(lock0&,lock1&,…,lockN&) 函数模板:
(1)std::lock会尝试同时对多个可锁定对象加锁,它使用免死锁算法避免死锁。若任何一个不可用则阻塞。函数返回后,所有可锁定对象己全部加锁。如果期间发生异常,则该异常会被抛出,同时释放所有己获取的锁。
(2)由于std::lock是个函数模板,它会为可锁定对象加锁,但不会并自动解锁。因此需要为每个可锁定对象手工解锁,也可以用带有adopt_lock参数的std::lock_guard,或者使用defer_lock参数的unique_lock来辅助解锁操作。
2. std::scoped_lock类模板(C++17支持)
// CLASS TEMPLATE scoped_lock template <class... _Mutexes> class scoped_lock { // class with destructor that unlocks mutexes public: explicit scoped_lock(_Mutexes&... _Mtxes) : _MyMutexes(_Mtxes...) { // construct and lock _STD lock(_Mtxes...); } explicit scoped_lock(adopt_lock_t, _Mutexes&... _Mtxes) : _MyMutexes(_Mtxes...) { // construct but don‘t lock } ~scoped_lock() noexcept { // unlock all _For_each_tuple_element(_MyMutexes, [](auto& _Mutex) noexcept { _Mutex.unlock(); }); } scoped_lock(const scoped_lock&) = delete; scoped_lock& operator=(const scoped_lock&) = delete; private: tuple<_Mutexes&...> _MyMutexes; };
(1)是个类模板,它封装了对std::lock的操作,可以避免对多个互斥量加锁时造成的死锁现象。同时利用RAII技术,实现对象析构时自动对这些互斥量进行解锁。
(2)构造函数:
①scoped_lock(_Mutexes&... _Mtxes):构造scoped_lock,并上锁。
②scoped_lock(adopt_lock_t, _Mutexes&... _Mtxes):构造但不上锁。
(3)scoped_lock对象不可复制和移动
【编程实验】自解锁系列
#include <iostream> #include <mutex> #include <thread> #include <chrono> #include <vector> using namespace std; //1. std::lock与std::lock_guard或std::unique_lock配合使用 struct Box { explicit Box(int num) : num_things(num) {}; int num_things; std::mutex m; }; void transfer(Box& from, Box& to, int num) { //1. 方法1:使用lock_guard为多个mutex加锁 //std::lock(from.m, to.m); //注意,这里传入的是mutex对象。(要先加锁) //std::lock_guard<std::mutex> lck1(from.m, std::adopt_lock); //adopt_lock:己假定from.m被上锁 //std::lock_guard<std::mutex> lck2(to.m, std::adopt_lock); //2. 方法2:使用unique_lock为多个mutex加锁 //必须使用std::defer_lock,而不是std::adopt_lock参数。因为当传入adopt_lock时,表示己假定事先己上锁, //就不能再为其加锁,否则会抛出异常。而std::defer_lock表示加锁操作会延迟到后续调用std::lock()时。 std::unique_lock<std::mutex> lck1(from.m, std::defer_lock); //defer:未实际加锁 std::unique_lock<std::mutex> lck2(to.m, std::defer_lock); //同上 //同时锁定两个lock,而不死锁 std::lock(lck1, lck2); //会调用lck1和lck2的lock()。注意,这里传入的是unique_lock对象 from.num_things -= num; to.num_things += num; //from.m 与to.m的互斥解锁于unique_lock或lock_guard的析构函数 } //2. 使用std::scoped_lock来锁定多个互斥量 struct Employee { std::string id; std::vector<std::string> lunch_partners; //午餐合伙人 std::mutex m; std::string output() const { std::string ret = "Employee " + id + " has lunch partners: "; for (const auto& parner : lunch_partners) ret += parner + " "; return ret; } Employee(std::string id) :id(id){} }; void send_mail(Employee&, Employee&) { //模拟耗时的发信操作 std::this_thread::sleep_for(std::chrono::seconds(1)); } //分配午餐合伙人 void assign_lunch_partner(Employee& e1, Employee& e2) { static std::mutex io_mutex; //用于同步std::cout/std::endl操作 { std::lock_guard<std::mutex> lk(io_mutex); std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl; } //scoped_lock可以避免像使用std::lock/std::unique_lock那样繁琐的使用方式。 //使用scoped_lock可同时获取两个锁,而不必担心对assign_lunch_partner的其它调用会造成死锁 { std::scoped_lock lk(e1.m, e2.m); { std::lock_guard<std::mutex> lk(io_mutex); std::cout << e1.id << " and " << e2.id << " got locks" << std::endl; } e1.lunch_partners.push_back(e2.id); e2.lunch_partners.push_back(e1.id); } send_mail(e1, e2); send_mail(e2, e1); } int main() { //1. std::lock的使用(配合std::lock_guard或std::unique_lock使用) Box b1(100); Box b2(50); std::thread t1(transfer, std::ref(b1), std::ref(b2), 10); std::thread t2(transfer, std::ref(b1), std::ref(b2), 5); t1.join(); t2.join(); std::cout << "box 1 num = " << b1.num_things << std::endl; std::cout << "box 2 num = " << b2.num_things << std::endl; //2. scoped_lock的使用: Employee alice("alice"), bob("bob"), christina("christina"), dave("dave"); //在线程中指派合伙人,因为内部的发邮件给用户操作会消耗较长时间。 std::vector<std::thread> threads; threads.emplace_back(assign_lunch_partner, std::ref(alice), std::ref(bob)); threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(bob)); threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(alice)); threads.emplace_back(assign_lunch_partner, std::ref(dave), std::ref(bob)); for (auto& th : threads) th.join(); std::cout << alice.output() << endl; std::cout << bob.output() << endl; std::cout << christina.output() << endl; std::cout << dave.output() << endl; return 0; } /*输出结果 box 1 num = 85 box 2 num = 65 alice and bob are waiting for locks alice and bob got locks christina and alice are waiting for locks christina and alice got locks christina and bob are waiting for locks christina and bob got locks dave and bob are waiting for locks dave and bob got locks Employee alice has lunch partners: bob christina Employee bob has lunch partners: alice christina dave Employee christina has lunch partners: alice bob Employee dave has lunch partners: bob */
以上是关于第29课 互斥量与自解锁(std::mutex和lock系列)的主要内容,如果未能解决你的问题,请参考以下文章
MinGW 64 中 std::mutex 和 QMutex 的性能(posix 线程版本)
C++11:互斥锁std::mutex和std::lock_guard/std::unique_lock