unique_lock详解

六、unique_lock详解

本节详细记录std::unique_lock类模板。

std::unique_lock是一个类模板,工作中一般使用lock_guardlock_guard取代了mutexlock()unlock(),unique_lock也是实现自动加锁的,相比于lock_guard它更加灵活,但是也更消耗资源。默认构造的情况下,unique_lock可以完全取代lock_guard

1、unique_lock的第二参数

1.1、adopt_lock

std::adopt_lock:表示这个互斥量已经被lock,(必须保证互斥量提前被lock了,才能使用此参数,否则会报异常)

1
2
3
my_mutex2_.lock(); // 一定要先lock()才能使用std::adopt_lock参数
//std::lock_guard<std::mutex> auto_mutex_2(my_mutex2_, std::adopt_lock); //std::adopt_lock标记
std::unique_guard<std::mutex> auto_mutex_2(my_mutex2_, std::adopt_lock);

1.2、try_to_lock

std::try_to_lock:使用try_to_lock前提是不能自己先去lock。此标志表示unique_lock会尝试用mutex的lock()去锁这个mutex,但如果没有锁定成功,会立即返回,并不会阻塞在哪里。下面程序展示一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void inMsgRecvQueue()  // 线程B入口函数
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
std::unique_lock<std::mutex> auto_mutex_1(my_mutex1_,std::try_to_lock);
if (auto_mutex_1.owns_lock())
{
// 拿到了锁头
msgRecvQueue_.push_back(i); // 操作共享数据
//其他处理...
}
else
{
// 没有拿到锁头
cout << "outMsgRecvQueue执行,但是没有拿到锁,只能做点别的事情" << i << endl;
}
}
}
bool outMsgLULProc(int &command) // 线程A入口函数
{
std::unique_lock<std::mutex> auto_mutex_1(my_mutex1_);
std::chrono::milliseconds dura(20000); // 20s
std::this_thread::sleep_for(dura); // 线程休息20s
if (!msgRecvQueue_.empty())
{
command = msgRecvQueue_.front();
msgRecvQueue_.pop_front();
return true;
}
else
return false;
}

上述两个函数是两个线程的入口函数,在其中一个线程A中,A拿到锁后线程sleep了20s,若使用传统的mutex,线程B因为拿不到锁也会阻塞至少20s。使用了std::try_to_lock参数后,线程B会尝试拿锁,在未拿到锁头的情况下线程B不会阻塞,我们可以去处理一些非共享的数据。

1.3、defer_lock

std::defer_lock:使用的前提是不能自己先lock,否则会报异常。它的意思是并没有给mutex加锁,初始化了一个没有加锁的mutex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void inMsgRecvQueue()  // 线程入口函数
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
std::unique_lock<std::mutex> auto_mutex_1(my_mutex1_,std::defer_lock); //创建一个没有加锁的mutex,相当于my_mutex1_与auto_mutex_1绑定在一起了
auto_mutex_1.lock(); // 没有必要自己解锁,auto_mutex_1会自动析构解锁
// ...处理共享代码
auto_mutex_1.unlock();
// 处理非共享代码
auto_mutex_1.lock();
msgRecvQueue_.push_back(i); //操作共享数据
// auto_mutex_1.unlock(); //可以自己unlock但是没必要
}
}

2、unique_lock的成员函数

2.1、lock()

在如上程序中的演示。

2.2、unlock()

在如上程序中的演示。lock锁住的代码越少,执行越快,程序的效率越高,有人也把锁头锁住的代码多少叫做粒度

  • 锁住的代码少,粒度细,执行效率高;
  • 锁住的代码多,粒度粗,执行效率低;

2.3、try_lock()

尝试给互斥量加锁,如果拿不到锁,则返回false,否则返回true,这个函数不阻塞。(和第二个参数中的std::try_to_lock相似)。用法如下:

1
2
3
4
5
6
7
8
9
10
11
std::unique_lock<std::mutex> auto_mutex_1(my_mutex1_,std::defer_lock); 
if (auto_mutex_1.try_lock())
{
// 拿到了锁头
msgRecvQueue_.push_back(i); // 操作共享数据
}
else
{
// 没有拿到锁头
cout << "outMsgRecvQueue执行,但是没有拿到锁,只能做点别的事情" << i << endl;
}

2.4、release()

返回它所管理的mutex对象指针,并释放所有权,等同于unique_lockmutex不在有联系。如果原来mutex对象处于加锁状态,有责任接管过来后负责解锁。用法:

1
2
3
4
std::unique_lock<std::mutex> auto_mutex_1(my_mutex1_); // 默认情况下构造时加锁
std::mutex* mutex_ptr = auto_mutex_1.release(); // 释放所有权
// 操作共享数据
mutex_ptr->unlock(); // 释放所有权之后有义务自己解锁

3、unique_lock所有权的传递

1
std::unique_lock<std::mutex> auto_mutex_1(my_mutex1_);

此时auto_mutex_1拥有my_mutex1_的所有权,auto_mutex_1可以把自己对mutex(my_mutex_1)的所有权转移给其他的unique_lock对象;unique_lock对mutex的所有权可以转移,但是不可以复制!

通过移动语义转移所有权:

1
2
3
std::unique_lock<std::mutex> auto_mutex_1(my_mutex1_);
// std::unique_lock<std::mutex> auto_mutex_1(auto_mutex_1); // 直接复制,编译报错
std::unique_lock<std::mutex> auto_mutex_2(std::move(auto_mutex_1)); // 左值转右值,调用移动构造函数

通过成员函数返回临时对象:

1
2
3
4
5
6
7
8
std::unique_lock<std::mutex> rtn_unique_lock()
{
std::unique_lock<std::mutex> tmpguard(my_mutex1_);
return tmpguard;
}
// 从一个临时对象来构造新的Foo的话,编译器会优先调用搬移构造函数,来把临时对象开膛破肚,取出自己需要的东西。
// 这里本质也是用了移动语义
std::unique_lock<std::mutex> auto_mutex_2 = rtn_unique_lock()
听说打赏我的人,最后都找到了真爱。