unique_lock
前面有一篇讲了使用lock_guard替代lock()和unlock(),可以避免忘记unlock()
一.unique_lock取代lock_guard
unique_lock是个类模板,一般使用lock_guard足够(取代mutex的lock()和unlock(),推荐使用)
unique_lock比lock_guard灵活很多,效率上差一点,内存占用多一点
下面使用unique_lock替代lock_guard测试:
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
class A
{
public:
//线程:把收到的消息(玩家命令)放入一个队列
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue(),插入一个元素" << i << endl;
//std::lock_guard<std::mutex> myGuard(myMutex); //使用lock_guard避免忘记unlock
std::unique_lock<std::mutex> myUniqueLock(myMutex); //使用unique_lock替代lock_guard
msgRecvQueue.push_back(i); //假设i就是收到的命令,存入消息队列
}
return;
}
//判断消息是否为空,不为空取出命令
bool outMsgProc(int& command)
{
//std::lock_guard<std::mutex> myGuard(myMutex); //使用lock_guard避免忘记unlock
std::unique_lock<std::mutex> myUniqueLock(myMutex); //使用unique_lock替代lock_guard
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
//处理取出的数据
return true;
}
return false;
}
//线程:把收到的命令取出
void outMsgRecvQueue()
{
int command;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
//处理命令
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //存放玩家发送的命令队列(共享数据)
mutex myMutex; //创建了一个互斥量(锁1)
};
int main(int argc, char** argv)
{
A obj;
std::thread myOutMsgObj(&A::outMsgRecvQueue, &obj);
//第二个参数为引用,保证子线程操作的是主线程中的obj,不会复制(可看上一篇,第二篇博客)
std::thread myInMsgObj(&A::inMsgRecvQueue, &obj); //第二个参数未引用
myOutMsgObj.join(); //阻塞主线程并等待子线程执行完毕
myInMsgObj.join();
return 0;
}
上述程序使用unique_lock替代lock_guard,程序正常执行。
二.unique_lock的第二个参数:
lock_guard可以带第二个参数:如std::lock_guard<std::mutex> myGuard(myMutex,std::adpot_lock); //adopt_lock起标记作用
2.1)std::adopt_lock参数:表示这个互斥量已经被lock()(必须要把互斥量提前lock,否则会报出异常)
std::adopt_lock标记的效果就是 假设调用方线程已经拥有了互斥的所有权(已经lock()成功了);
通知lock_guard不需要在构造函数中lock()这个互斥量了
unique_lock也可以带std::adopt_lock标记,含义与lock_guard中的相同,即不希望在unique_lock()的构造函数中lock这个mutex
//上面的程序中修改线程函数部分,并进行测试:
class A
{
public:
//线程:把收到的消息(玩家命令)放入一个队列
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue(),插入一个元素" << i << endl;
//std::lock_guard<std::mutex> myGuard(myMutex); //使用lock_guard避免忘记unlock
//std::unique_lock<std::mutex> myUniqueLock(myMutex); //使用unique_lock替代lock_guard
myMutex.lock();
std::unique_lock<std::mutex> myUniqueLock(myMutex, std::adopt_lock);
//带std::adopt_lock参数必须提前lock()
msgRecvQueue.push_back(i); //假设i就是收到的命令,存入消息队列
}
return;
}
//判断消息是否为空,不为空取出命令
bool outMsgProc(int& command)
{
//std::lock_guard<std::mutex> myGuard(myMutex); //使用lock_guard避免忘记unlock
//std::unique_lock<std::mutex> myUniqueLock(myMutex); //使用unique_lock替代lock_guard
myMutex.lock();
std::unique_lock<std::mutex> myUniqueLock(myMutex, std::adopt_lock);
//带std::adopt_lock参数必须提前lock()
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
//处理取出的数据
return true;
}
return false;
}
//线程:把收到的命令取出
void outMsgRecvQueue()
{
int command;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
//处理命令
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //存放玩家发送的命令队列(共享数据)
mutex myMutex; //创建了一个互斥量(锁1)
};
执行如上程序,正常执行。如果去掉myMutex.lock(),即未提前lock(),程序会报出异常。
2.2)std::try_to_lock参数
首先我们做一个有趣的测试:向其中一个线程加入5秒钟的延时,即让这个线程停止运行5秒钟,但我们会发现当给这个线程延时5秒钟后,另一个也会相应的延时5秒钟
分析:因为这个线程已经拿到锁了(即lock()成功),进入了延时,而还未unlock,另一个线程自然就无法获取到锁(lock()失败),也只能停止运行,等延时过后,lock()成功的线程(代码加入延时的线程)执行完unlock(),另一个就可以lock()成功了。
class A
{
public:
//线程:把收到的消息(玩家命令)放入一个队列
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue(),插入一个元素" << i << endl;
myMutex.lock();
std::unique_lock<std::mutex> myUniqueLock(myMutex, std::adopt_lock);
//带std::adopt_lock参数必须提前lock()
//std::chrono::milliseconds duration(20000); //20000毫秒=20秒
//std::this_thread::sleep_for(duration); //停止运行20秒
msgRecvQueue.push_back(i); //假设i就是收到的命令,存入消息队列
}
return;
}
//判断消息是否为空,不为空取出命令
bool outMsgProc(int& command)
{
myMutex.lock();
std::unique_lock<std::mutex> myUniqueLock(myMutex, std::adopt_lock);
//带std::adopt_lock参数必须提前lock()
std::chrono::milliseconds duration(5000); //5000毫秒=5秒
std::this_thread::sleep_for(duration); //停止运行5秒
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
//处理取出的数据
return true;
}
return false;
}
//线程:把收到的命令取出
void outMsgRecvQueue()
{
int command;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
//处理命令
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //存放玩家发送的命令队列(共享数据)
mutex myMutex; //创建了一个互斥量(锁1)
};
下面是程序的执行结果,可以看出每过5秒钟,两个线程继续执行。



std::try_to_lock的作用:尝试lock(),但如果没有锁定成功,也会立即返回(可以查看是否lock()成功),并且不会阻塞在那里
用这个try_to_lock的前提是不能先去lock()
//下面我们使用try_to_lock测试(避免上面的情况出现,即一个程序延时,另一个程序也延时,这里,我们会让另一个线程去执行其他任务,不会一直卡死):
修改线程函数如下:
class A
{
public:
//线程:把收到的消息(玩家命令)放入一个队列
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue(),插入一个元素" << i << endl;
std::unique_lock<std::mutex> myUniqueLock(myMutex, std::try_to_lock);
//使用try_to_lock()参数,前面如果已经lock(),程序会崩溃
if (myUniqueLock.owns_lock()) //判断是否拿到锁
{
//拿到了锁:lock成功
msgRecvQueue.push_back(i); //假设i就是收到的命令,存入消息队列
}
else
{
//没拿到锁
cout << "inMsgRecvQueue()执行,但没有拿到锁..." << i << endl;
}
}
return;
}
//判断消息是否为空,不为空取出命令
bool outMsgProc(int& command)
{
myMutex.lock();
std::unique_lock<std::mutex> myUniqueLock(myMutex, std::adopt_lock);
//带std::adopt_lock参数必须提前lock()
std::chrono::milliseconds duration(5000); //5000毫秒=5秒
std::this_thread::sleep_for(duration); //停止运行5秒
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
//处理取出的数据
return true;
}
return false;
}
//线程:把收到的命令取出
void outMsgRecvQueue()
{
int command;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
//处理命令
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //存放玩家发送的命令队列(共享数据)
mutex myMutex; //创建了一个互斥量(锁1)
};
执行上面的程序,如下图所示,发现另一个线程(代码未加入延时的线程)不会卡死,而是一直在执行其他任务,这就是使用try_to_lock的好处。
注意:使用try_to_lock不能在前面lock()

2.3)std::defer_lock参数
使用std::defer_lock的前提是也不能先执行lock(),否则会报出异常
defer_lock的意思就是初始化了一个没有加锁的mutex
相当于将unique_lock和mutex绑定在一起,用unique_lock管理mutex
借着defer_lock的话题,介绍一些unique_lock的重要成员函数,请看下文
三.unique_lock的成员函数
3.1)lock(),加锁
修改线程函数如下:
class A
{
public:
//线程:把收到的消息(玩家命令)放入一个队列
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue(),插入一个元素" << i << endl;
std::unique_lock<std::mutex> myUniqueLock(myMutex, std::defer_lock);
//创建没有加锁的myMutex,也不能提前lock()
myUniqueLock.lock(); //注意不是mutex的成员函数lock(),且之后不用自己手动unlock()
msgRecvQueue.push_back(i); //假设i就是收到的命令,存入消息队列
}
return;
}
//判断消息是否为空,不为空取出命令
bool outMsgProc(int& command)
{
myMutex.lock();
std::unique_lock<std::mutex> myUniqueLock(myMutex, std::adopt_lock);
//带std::adopt_lock参数必须提前lock()
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
//处理取出的数据
return true;
}
return false;
}
//线程:把收到的命令取出
void outMsgRecvQueue()
{
int command;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
//处理命令
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //存放玩家发送的命令队列(共享数据)
mutex myMutex; //创建了一个互斥量(锁1)
};
使用修改后的线程函数,程序依然可以正常执行,注意此处的介绍的lock()函数并不是mutex类的成员函数,且之后不用自己手动unlock()(当然也可以自己手动unlock())
3.2)unlock()
疑惑:明明可以自动解锁,为什么还要有unlock()
解答:使用unlock()可以在程序中临时处理一些非共享代码,处理完之后再lock()。,lock()锁住的代码段越少,程序运行效率越高
当然,虽然上面的程序测试了使用lock()时不用自己手动unlock(),但也可以自己手动unlock()(画蛇添足)
lock()锁住的代码段的多少称为锁的 粒度,粒度一般用粗细来描述
a)lock()锁住的代码少,这个粒度叫细,执行效率高。
b)lock()锁住的代码多,这个粒度叫粗,执行效率低。
尽量选择合适粒度的代码进行保护,粒度太细,可能漏掉共享数据的保护,粒度太粗,影响效率。
修改后的线程函数如下:
class A
{
public:
//线程:把收到的消息(玩家命令)放入一个队列
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue(),插入一个元素" << i << endl;
std::unique_lock<std::mutex> myUniqueLock(myMutex, std::defer_lock);
//创建没有加锁的myMutex,也不能提前lock()
myUniqueLock.lock(); //注意不是mutex的成员函数lock(),且之后不用自己手动unlock()
//下方处理共享代码
//...
//因为有一些非共享代码要处理
myUniqueLock.unlock();
//下方处理非共享代码
//...
//处理完之后再进行lock()
myUniqueLock.lock();
//下方可处理共享代码
//...
msgRecvQueue.push_back(i); //假设i就是收到的命令,存入消息队列
myUniqueLock.unlock(); //画蛇添足,但也可以
}
return;
}
//判断消息是否为空,不为空取出命令
bool outMsgProc(int& command)
{
myMutex.lock();
std::unique_lock<std::mutex> myUniqueLock(myMutex, std::adopt_lock);
//带std::adopt_lock参数必须提前lock()
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
//处理取出的数据
return true;
}
return false;
}
//线程:把收到的命令取出
void outMsgRecvQueue()
{
int command;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
//处理命令
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //存放玩家发送的命令队列(共享数据)
mutex myMutex; //创建了一个互斥量(锁1)
};
使用修改后的线程函数运行程序,演示了unlock()函数怎么用来临时处理非共享代码。
3.3)try_lock()
类似于mutex类的try_to_lock()函数,尝试给互斥量加锁,如果拿不到锁(lock()失败),则返回false,否则返回true,这个函数是不阻塞的
修改后的线程函数如下:
class A
{
public:
//线程:把收到的消息(玩家命令)放入一个队列
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue(),插入一个元素" << i << endl;
std::unique_lock<std::mutex> myUniqueLock(myMutex, std::defer_lock);
//创建没有加锁的myMutex,也不能提前lock()
if (myUniqueLock.try_lock()) //返回true表示达到锁(lock()成功)
{
msgRecvQueue.push_back(i); //假设i就是收到的命令,存入消息队列
}
else
{
//返回false(lock()失败)
cout << "inMsgRecvQueue()执行,但没有拿到锁" << i << endl;
}
}
return;
}
//判断消息是否为空,不为空取出命令
bool outMsgProc(int& command)
{
myMutex.lock();
std::unique_lock<std::mutex> myUniqueLock(myMutex, std::adopt_lock);
//带std::adopt_lock参数必须提前lock()
std::chrono::milliseconds duration(5000); //5000毫秒=5秒
std::this_thread::sleep_for(duration); //停止运行5秒
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
//处理取出的数据
return true;
}
return false;
}
//线程:把收到的命令取出
void outMsgRecvQueue()
{
int command;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
//处理命令
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //存放玩家发送的命令队列(共享数据)
mutex myMutex; //创建了一个互斥量(锁1)
};
上面的程序在其中一个线程延时了5秒,且还没有unlock(),另一个程序在这5秒之内无法获得锁(无法lock()成功),使用try_lock()不断尝试加锁,5秒过后可获得锁(lock()成功)。
3.4)release()
返回它所管理的mutex对象指针,并释放所有权,即unique_lock和mutex不再有关系
不要混淆unlock()和release()
如果原来mutex对象处于加锁状态,需要手动解锁。
修改后的线程函数如下:
class A
{
public:
//线程:把收到的消息(玩家命令)放入一个队列
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue(),插入一个元素" << i << endl;
std::unique_lock<std::mutex> myUniqueLock(myMutex); //相当于mutex已经lock()
std::mutex* ptrMutex = myUniqueLock.release();
//将unique_lock和mutex分离,现在需手动解锁mutex(unlock()),返回指向原来mutex的指针
msgRecvQueue.push_back(i); //假设i就是收到的命令,存入消息队列
ptrMutex->unlock(); //自己需手动unlock()
}
return;
}
//判断消息是否为空,不为空取出命令
bool outMsgProc(int& command)
{
myMutex.lock();
std::unique_lock<std::mutex> myUniqueLock(myMutex, std::adopt_lock);
//带std::adopt_lock参数必须提前lock()
std::chrono::milliseconds duration(5000); //5000毫秒=5秒
std::this_thread::sleep_for(duration); //停止运行5秒
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
//处理取出的数据
return true;
}
return false;
}
//线程:把收到的命令取出
void outMsgRecvQueue()
{
int command;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
//处理命令
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //存放玩家发送的命令队列(共享数据)
mutex myMutex; //创建了一个互斥量(锁1)
};
上述程序使用releas()分离了unique_lock和mutex后,后面需手动解锁(unlock())
四.unique_lock所有权的传递(一个unique_lock对应一个mutex)
std::unique_lock<std::mutex> myUniqueLock(myMutex);
所有权概念:myUniqueLock拥有myMutex的所有权
myUniqueLock可以把自己对myMutex的所有权转移给其他的unique_lock对象
所有unique_lock对象的mutex的所有权可以转移,但是不能复制。类似于智能指针unique_ptr
下面修改的线程函数演示了所有权转移的方法,以及验证了所有权不能复制。
a)使用std::move转移
class A
{
public:
//线程:把收到的消息(玩家命令)放入一个队列
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue(),插入一个元素" << i << endl;
std::unique_lock<std::mutex> myUniqueLock1(myMutex); //相当于mutex已经lock()
//std::unique_lock<std::mutex> myUniqueLock2(myMutex);
//再使用myMutex创建一个unique_lock对象,会发现出错,相当于lock()两次,程序崩溃
//std::unique_lock<std::mutex> myUniqueLock2(myUniqueLock);
//复制myMutex的所有权,会发现直接报错,此操作非法
std::unique_lock<std::mutex> myUniqueLock2(std::move(myUniqueLock1));
//将myMutex的所有权传递给myUniqueLock2,相当于myUniqueLock2与myMutex现在绑定在一起
//而myUniqueLock1指向空,相当于release()
msgRecvQueue.push_back(i); //假设i就是收到的命令,存入消息队列
}
return;
}
//判断消息是否为空,不为空取出命令
bool outMsgProc(int& command)
{
myMutex.lock();
std::unique_lock<std::mutex> myUniqueLock(myMutex, std::adopt_lock);
//带std::adopt_lock参数必须提前lock()
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
//处理取出的数据
return true;
}
return false;
}
//线程:把收到的命令取出
void outMsgRecvQueue()
{
int command;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
//处理命令
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //存放玩家发送的命令队列(共享数据)
mutex myMutex; //创建了一个互斥量(锁1)
};
//b)return std::unique_lock<std::mutex>将临时对象拥有的所有权进行转移
下面修改的线程函数采取返回临时对象的方法转移所有权。
class A
{
public:
std::unique_lock<std::mutex> retUniqueLock()
{
std::unique_lock<std::mutex> tmpUniqueLock(myMutex);
return tmpUniqueLock;
//返回一个局部的unique_lock对象是可以的,系统生成临时unique_lock对象,并调用unique_lock的移动构造函数
}
//线程:把收到的消息(玩家命令)放入一个队列
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue(),插入一个元素" << i << endl;
std::unique_lock<std::mutex> myUniqueLock = retUniqueLock();//所有权转移
msgRecvQueue.push_back(i); //假设i就是收到的命令,存入消息队列
}
return;
}
//判断消息是否为空,不为空取出命令
bool outMsgProc(int& command)
{
myMutex.lock();
std::unique_lock<std::mutex> myUniqueLock(myMutex, std::adopt_lock);
//带std::adopt_lock参数必须提前lock()
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
//处理取出的数据
return true;
}
return false;
}
//线程:把收到的命令取出
void outMsgRecvQueue()
{
int command;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
//处理命令
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //存放玩家发送的命令队列(共享数据)
mutex myMutex; //创建了一个互斥量(锁1)
};
网友评论