C++11 std::unique_lock与std::lock_guard区别及多线程应用实例

时间:2021-01-23 15:03:21

C++多线程编程中通常会对共享的数据进行写保护,以防止多线程在对共享数据成员进行读写时造成资源争抢导致程序出现未定义的行为。通常的做法是在修改共享数据成员的时候进行加锁--mutex。在使用锁的时候通常是在对共享数据进行修改之前进行lock操作,在写完之后再进行unlock操作,进场会出现由于疏忽导致由于lock之后在离开共享成员操作区域时忘记unlock,导致死锁。

针对以上的问题,C++11中引入了std::unique_lock与std::lock_guard两种数据结构。通过对lock和unlock进行一次薄的封装,实现自动unlock的功能。

 std::mutex mut;

 void insert_data()
{
std::lock_guard<std::mutex> lk(mut);
queue.push_back(data);
} void process_data()
{
std::unqiue_lock<std::mutex> lk(mut);
queue.pop();
}

std::unique_lock 与std::lock_guard都能实现自动加锁与解锁功能,但是std::unique_lock要比std::lock_guard更灵活,但是更灵活的代价是占用空间相对更大一点且相对更慢一点。

1 回顾采用RAII手法管理mutex的std::lock_guard其功能是在对象构造时将mutex加锁,析构时对mutex解锁,这样一个栈对象保证了在异常情形下mutex可以在lock_guard对象析构被解锁,lock_guard拥有mutex的所有权。

 explicit lock_guard (mutex_type& m);//必须要传递一个mutex作为构造参数
lock_guard (mutex_type& m, adopt_lock_t tag);//tag=adopt_lock表示mutex已经在之前被上锁,这里lock_guard将拥有mutex的所有权
lock_guard (const lock_guard&) = delete;//不允许copy constructor

2 再来看一个与std::lock_guard功能相似但功能更加灵活的管理mutex的对象 std::unique_lock,unique_lock内部持有mutex的状态:locked,unlocked。unique_lock比lock_guard占用空间和速度慢一些,因为其要维护mutex的状态。

  unique_lock() noexcept;    //可以构造一个空的unique_lock对象,此时并不拥有任何mutex

  explicit unique_lock (mutex_type& m);//拥有mutex,并调用mutex.lock()对其上锁    

  unique_lock (mutex_type& m, try_to_lock_t tag);//tag=try_lock表示调用mutex.try_lock()尝试加锁

  unique_lock (mutex_type& m, defer_lock_t tag) noexcept;//tag=defer_lock表示不对mutex加锁,只管理mutex,此时mutex应该是没有加锁的

  unique_lock (mutex_type& m, adopt_lock_t tag);//tag=adopt_lock表示mutex在此之前已经被上锁,此时unique_locl管理mutex

  template <class Rep, class Period>
unique_lock (mutex_type& m, const chrono::duration<Rep,Period>& rel_time);//在一段时间rel_time内尝试对mutex加锁,mutex.try_lock_for(rel_time) template <class Clock, class Duration>
unique_lock (mutex_type& m, const chrono::time_point<Clock,Duration>& abs_time);//mutex.try_lock_until(abs_time)直到abs_time尝试加锁 unique_lock (const unique_lock&) = delete;//禁止拷贝构造 unique_lock (unique_lock&& x);//获得x管理的mutex,此后x不再和mutex相关,x此后相当于一个默认构造的unique_lock,移动构造函数,具备移动语义,movable but not copyable

说明:其中2和5拥有mutex的所有权,而1和4永远不用有mutex的所有权,3和6及7若尝试加锁成功则拥有mutex的所有权

unique_lock 在使用上比lock_guard更具有弹性,和 lock_guard 相比,unique_lock 主要的特色在于:
         unique_lock 不一定要拥有 mutex,所以可以透过 default constructor 建立出一个空的 unique_lock。
         unique_lock 虽然一样不可复制(non-copyable),但是它是可以转移的(movable)。所以,unique_lock 不但可以被函数回传,也可以放到 STL 的 container 里。
         另外,unique_lock 也有提供 lock()、unlock() 等函数,可以用来加锁解锁mutex,也算是功能比较完整的地方。
         unique_lock本身还可以用于std::lock参数,因为其具备lock、unlock、try_lock成员函数,这些函数不仅完成针对mutex的操作还要更新mutex的状态。

3  std::unique_lock其它成员函数

 ~unique_lock();//若unique_lock对象拥有管理的mutex的所有权,mutex没有被销毁或者unlock,那么将执行mutex::unlock()解锁,并不销毁mutex对象。
mutex_type* mutex() const noexcept;//返回unique_lock管理的mutex指针,但是unique_lock不会放弃对mutex的管理,若unique_lock对mutex上锁了,其有义务对mutex解锁
bool owns_lock() const noexcept;//当mutex被unique_lock上锁,且mutex没有解锁或析构,返回真,否则返回false
explicit operator bool() const noexcept;//同上

4  std::unique_lock增加了灵活性,比如可以对mutex的管理从一个scope通过move语义转到另一个scope,不像lock_guard只能在一个scope中生存。同时也增加了管理的难度,因此如无必要还是用lock_guard。

5 网上看见一个unique_lock的应用于银行转账的实例,贴在这里:

 #include <mutex>
#include <thread>
#include <chrono>
#include <iostream>
#include <string>
using namespace std;
struct bank_account//银行账户
{
explicit bank_account(string name, int money)
{
sName = name;
iMoney = money;
} string sName;
int iMoney;
mutex mMutex;//账户都有一个锁mutex
};
void transfer( bank_account &from, bank_account &to, int amount )//这里缺少一个from==to的条件判断个人觉得
{
unique_lock<mutex> lock1( from.mMutex, defer_lock );//defer_lock表示延迟加锁,此处只管理mutex
unique_lock<mutex> lock2( to.mMutex, defer_lock );
lock( lock1, lock2 );//lock一次性锁住多个mutex防止deadlock
from.iMoney -= amount;
to.iMoney += amount;
cout << "Transfer " << amount << " from "<< from.sName << " to " << to.sName << endl;
}
int main()
{
bank_account Account1( "User1", );
bank_account Account2( "User2", );
thread t1( [&](){ transfer( Account1, Account2, ); } );//lambda表达式
thread t2( [&](){ transfer( Account2, Account1, ); } );
t1.join();
t2.join();
}

说明:加锁的时候为什么不是如下这样的?在前面一篇博文中有讲到多个语句加锁可能导致deadlock,假设:同一时刻A向B转账,B也向A转账,那么先持有自己的锁再相互请求对方的锁必然deadlock。

 lock_guard<mutex> lock1( from.mMutex );
lock_guard<mutex> lock2( to.mMutex );

采用lock_guard也可以如下:

 lock( from.mMutex, to.mMutex );
lock_guard<mutex> lock1( from.mMutex, adopt_lock );//adopt_lock表示mutex已经上锁,lock1将拥有from.mMutex
lock_guard<mutex> lock2( to.mMutex, adopt_lock );

6 上面的例子lock针对mutex加锁后,并没有显示解锁,那么离开lock的作用域后解锁了吗?验证代码如下,在lock后抛出异常mutex解锁了吗?:

 #include<mutex>
#include<exception>
#include<iostream>
using namespace std;
int main(){
mutex one,two;
try{
{
lock(one,two);
throw ;
cout<<"locking..."<<endl;
}
}catch(int){
cout<<"catch..."<<endl;
}
if(!one.try_lock()&&!two.try_lock())
cout<<"failure"<<endl;
else
cout<<"success"<<endl;
return ;
}

程序输出:

catch...
success          //lock后的操作抛出异常后,mutex解锁了

7 unique_lock is movable but not copyable.因此可以作为函数返回值,STL容器元素。例如:一个函数采用unique_lock加锁mutex然后准备好数据并将unique_lock返回给调用者,调用者在mutex保护下对数据进一步加工,简单的代码如下:

 #include<mutex>
#include<iostream>
using namespace std;
mutex m;
unique_lock<mutex> get_lock(){
unique_lock<mutex> lk(m);
cout<<"prepare data..."<<endl;//准备数据
return lk;//移动构造
}
int main(){
unique_lock<mutex> lk(get_lock());
cout<<"process data..."<<endl;//在mutex保护下数据深加工
return ;
}

8 unique_lock::lock(), unique_lock::unlock()这一组成员函数充分说明了,unique_lock在构造时不必对mutex加锁且可以在后期某个时候对mutex加锁; unique_lock可以在自己实例销毁前调用unique_lock::unlock()提前释放锁,这对于一些分支语句中可能得到性能提升。