平时看代码时,也会使用到std::lock_guard,但是std::unique_lock用的比较少。在看并发编程,这里总结一下。方便后续使用。
std::unique_lock也可以提供自动加锁、解锁功能,比std::lock_guard更加灵活。
std::lock_guard是RAII模板类
的简单实现,功能简单。
1.std::lock_guard 在构造函数中进行加锁,析构函数中进行解锁。
2.锁在多线程编程中,使用较多,因此c++11提供了lock_guard模板类;在实际编程中,我们也可以根据自己的场景编写resource_guard
RAII类,避免忘掉释放资源。
下面是一个使用std::lock_guard的代码例子,1+2+ .. + 100的多线程实现,每个num只能由一个线程处理。:
#include
#include
#include
#include
#include
std::mutex my_lock;
void add(int &num, int &sum){
while(true){
std::lock_guard lock(my_lock);
if (num < 100){ //运行条件
num += 1;
sum += num;
}
else { //退出条件
break;
}
}
}
int main(){
int sum = 0;
int num = 0;
std::vector ver; //保存线程的vector
for(int i = 0; i < 20; ++i){
std::thread t = std::thread(add, std::ref(num), std::ref(sum));
ver.emplace_back(std::move(t)); //保存线程
}
std::for_each(ver.begin(), ver.end(), std::mem_fn(&std::thread::join)); //join
std::cout << sum << std::endl;
}
类 unique_lock 是通用互斥包装器,允许
延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用
。
unique_lock比lock_guard使用更加灵活,功能更加强大。
使用unique_lock需要付出更多的时间、性能成本。
下面是try_lock的使用例子。
#include // std::cout
#include // std::thread
#include // std::mutex, std::unique_lock
#include
std::mutex mtx; // mutex for critical section
std::once_flag flag;
void print_block (int n, char c) {
//unique_lock有多组构造函数, 这里std::defer_lock不设置锁状态
std::unique_lock my_lock (mtx, std::defer_lock);
//尝试加锁, 如果加锁成功则执行
//(适合定时执行一个job的场景, 一个线程执行就可以, 可以用更新时间戳辅助)
if(my_lock.try_lock()){
for (int i=0; i ver;
int num = 0;
for (auto i = 0; i < 10; ++i){
ver.emplace_back(print_block,50,'*');
ver.emplace_back(run_one, std::ref(num));
}
for (auto &t : ver){
t.join();
}
std::cout << num << std::endl;
return 0;
}
class A
{
public:
std::unique_lock itsUnique_lock()
{
std::unique_lock tempUnique_lock(my_mutex1);
return tempUnique_lock;
//从函数返回一个局部的unique_lock对象是可以的。
//返回这种局部对象,tempUnique_lock会调用unique_lock的移动构造函数
//在下面可以这样调用:
//std::unique_lock sbguard1 = itsUnique_lock();
//sbguard1 就有了my_mutex1的所有权
}
void inMsgRecvQueue() //unlock()
{
for(int i=0;i<10000;++i)
{
cout<<"inMsgRecvQueue():"< lguard(my_mutex,std::adopt_lock);
//使用std::adopt_lock的前提条件是my_mutex已经加锁成功了
//my_mutex.lock();下面一行是try_to_lock所以千万不能提前加锁,不然会为一个mutex对象加两次锁
std::unique_lock lguard(my_mutex,std::try_to_lock);
//使用std::adopt_lock的前提条件是my_mutex已经加锁成功了
if(lguard.owns_lock())
{
msgRecvQueue.push_back(i);//拿到了锁
}else
{
//没拿到锁
cout<<"inMsgRecvQueue()执行:但没有加锁成功,干点别的事"< lguard(my_mutex);
//std::lock_guard lguard1(my_mutex1);
//std::lock_guard lguard2(my_mutex2);
std::lock(my_mutex1,my_mutex2);//相当于每个互斥量都调用了lock(),下面的2个unlock()必须还要写
std::lock_guard lguard1(my_mutex1,std::adopt_lock);
//多了后面一个参数std::adopt_lock就可以不调用构造函数里mutex::lock()
std::lock_guard lguard2(my_mutex2,std::adopt_lock);
//而且使用了lock_guard就不需要在后面写unlock()了
std::lock(my_mutex1,my_mutex2);
//相当于每个互斥量都调用了lock(),下面的2个unlock()必须还要写
//my_mutex.lock();
my_mutex1.lock();
my_mutex2.lock();
msgRecvQueue.push_back(i);
//my_mutex.unlock();
my_mutex1.unlock();
my_mutex2.unlock();
}
}
bool outMsgProc(int &command)
{
std::unique_lock lguard(my_mutex);
std::lock_guard lguard(my_mutex1)//lguard是对象名
//lock_guard构造函数里执行了mutex::lock()
//lock_guard析构函数里执行了mutex::unlock()
//std::lock_guard lguard1(my_mutex1);
//std::lock_guard lguard2(my_mutex2);
std::lock(my_mutex1,my_mutex2);
//相当于每个互斥量都调用了lock(),下面的2个unlock()必须还要写
std::lock_guard lguard1(my_mutex1,std::adopt_lock);
//多了后面一个参数std::adopt_lock就可以不调用构造函数里mutex::lock()
std::lock_guard lguard2(my_mutex2,std::adopt_lock);
//使用std::adopt_lock的前提条件是my_mutex2已经加锁成功了
//my_mutex.lock();
//my_mutex1.lock();
//my_mutex2.lock();
std::chrono::milliseconds dura(20000);//一秒=1000,所以这是20秒
std::this_thread::sleep_for(dura);//休息一定时间
if(!msgRecvQueue.empty())
{
command =msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
msgRecvQueue.pop_front();//移除第一个元素
//my_mutex.unlock();
my_mutex1.unlock();
my_mutex2.unlock();
return true;
}
//my_mutex.unlock();
my_mutex1.unlock();//lock 的顺序需要一致
my_mutex2.unlock();
return false;
}
//遇到共享数据段的操作,就先lock() ,操作完了后,再unlock()
void outMsgRecvQueue()
{
int command =0;
for(int i=0;i<10000;++i)
{
bool result=outMsgProc(command);
if(result==true)
{
cout<<"outMsgRecvQueue()执行,取出一个元素"< msgRecvQueue;//容器(消息队列)专门用于代表玩家发送的命令
std::mutex my_mutex1;//创建了一个互斥量
std::mutex my_mutex2;//创建了一个互斥量
};
int main()
{
A myobja;
std::thread my_in_Thread(&A::outMsgRecvQueue,&myobja);
//第二个参数是引用,才能保证线程里,用的是同一个对象,不然会调用拷贝构造,进行深拷贝
std::thread my_out_Thread(&A::inMsgRecvQueue,&myobja);
outMsgRecvQueue.join();
inMsgRecvQueue.join();
//使用unique_lock(类模板)取代lock_guard,unique_lock(比ock_guard更灵活,但是效率降差一点,内存占用多一点
//工作中一般lock_guard是可以的。但是有些特殊需求会用到unique_lock
//unique_lock的第二个参数
//lock_guard的第二个参数
//std::lock_guard lguard1(my_mutex1,std::adopt_lock);//adopt_lock起标记作用
//2.1 std::adopt_lock::表示互斥量已经lock了,(你必须要把互斥量提前lock了,否则会报异常)
//std::adopt_lock标记的效果就是“假设调用的线程已经拥有量互斥量的使用权(已经lock成功了)”
//unique_lock也可以带std::adopt_lock这个标记,含义:就是不希望在unique_lock的构造函数中调用mutex的lock()了
cout<<"主线程收尾,正常退出"
//保护共享数据,操作时,某个线程用代码把共享数据锁住,操作数据解锁。
其他想操作共享数据的线程必须等待解锁,锁定住,操作,解锁。
互斥量;mutex
互斥量是一个类。理解成一把锁,多个线程尝试用lock()成员函数来加锁。只有一个线程可以锁定成功,(成功标志这个lock()成功返回了)
如果没加锁成功,那么流程就卡在lock()这里,直到加锁成功。
互斥量使用要小心,保护数据不多也不少。多了影响效率,少了起不到效果。
需要加入头文件#include
2.1 lock() ,unlock() 这两个必须成对使用,而且不允许调用两次lock()或者unlock()
步骤:先 lock() 然后操作共享数据, 再unlock()
用了lock() 但是忘记了unlock()的问题,有时候特别难排查,if的条件可能在特殊条件下才成立。
未来防止忘记unlock(),引入了std::lock_guard的类模板:它会替你unlock()
2.2 std::lock_guard类模板:直接取代lock() ,unlock()
std::lock_guard
两个线程A、B
(a)线程A执行的时候,这个线程先把a锁lock()成功,然后去锁b锁。线程A拿不到b锁就停在这里一直加锁
出现上下文切换
(b)线程B执行的时候,这个线程先把b锁lock()成功,然后去锁a锁。线程B拿不到a锁就停在这里一直加锁
这时死锁就产生了。
死锁的解决方案:两个互斥量的调用顺序必须保持一致
std::lock()函数模板 如果互斥量中有一个没锁住,它就在那里等,所有的锁都锁住才会往下走。
std::lock()作用:一次锁住两个或两个以上互斥量(至少两个,多了不限,1个不行)它不存在这种因为在多线程中因为锁的顺序问题导致的死锁问题,它就在那里等着,等所有互斥量都锁住才会继续往下走(返回)
要么互斥量都锁住,要么互斥量都没锁住,如果只锁一个,其他的没锁住,就会放开锁住的互斥量。
std::lock_guard的std::adopt_lock参数是结构体对象,作用:表示这个互斥量已经被lock()过了,
不需要在std::lock_guard
std::lock_guard
//lock_guard构造函数里执行了mutex::lock()
//lock_guard析构函数里执行了mutex::unlock()
std::lock_guard
多了后面一个参数std::adopt_lock就可以不调用构造函数里mutex::lock(),前面的代码已经lock()过了。
小结:std::lock()可以一次锁多个互斥量,如果有没锁住的就会放开。
(1)unique_lock取代lock_guard
(2)unique_lock的第二参数 std::adopt_lock 前面必须已经加锁了
std::try_to_lock 前面必须还没有被加锁
std::defer_lock
(3)unique_lock()的成员函数
3.1 lock() 加锁
3.2 unlock() 解锁
//因为lock锁住的代码段越少,执行越快,有的人把锁住的代码多少,称为锁的粒度,一般用粗细来描述。
锁住的代码少,这个粒度叫细,执行效率高。
锁住的代码多,这个粒度叫粗,执行效率低。
要学会选择合适的粒度来保护代码。
3.3 try_lock() 尝试给互斥量加锁,如果没加成功返回false 如果加锁成功返回true
3.4 release() 返回它管理的mutex指针,并释放所有权,也就是说unique_lock()和mutex不再有关系
严格区分unlock()和release(),不要混淆。如果原来mutex对象处于加锁状态,你使用了 release(),那么你有责任,负责解锁。
(4)unique_lock所有权的传递
unique_lock是和mutex绑定到一起的,unique_lock需要管理一个mutex对象
一个mutex对象只能与一个unique_lock绑定到一起。
我们会尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,我也会立即返回,并不会阻塞在那里
用try_to_lock的前提是你不能先去lock
defer_lock的意思,就是没有给mutext加锁,初始化了一个没有加锁的mutex
我们接着std::defer_lock的话题来介绍unique_lock的重要成员函数
std::unique_lock
std::defer_lock 前提是你自己不能先加锁,否则会报异常
defer_lock的意思,就是没有给mutext加锁,初始化了一个没有加锁的mutex
//lguard1对象拥有my_mutex1对象的所有权
lguard1.lock(); //不用自己unlock
//操作共享代码
lguard1.unlock();
//处理一些非共享代码
lguard1.lock();
//操作共享代码
msgRecvQueue.push_back(i);
std::unique_lock
lguard1.try_to_lock();
msgRecvQueue.push_back(i);
std::unique_lock
//unique_lock可以自动加锁,离开作用域时会检查有没有加锁,如果加了它会自动解锁,
也可以通过成员函数做灵活的加锁和解锁
std::mutex *p = lguard1.release();//使用了release后返回的是mutex的指针,现在你有责任自己解锁
msgRecvQueue.push_back(i);
p->unlock();//release后需要自己负责mutex的解锁
//转移所属权
1)使用std::move
2)return std::unique_lock
std::unique_lock
std::unique_lock
//移动语义,现在相当于lguard2和my_mutex1绑定到一起了,lguard1指向空
std::move()//可以使左值变右值,会调用移动构造函数。
std::unique_lock
{
std::unique_lock
return tempUnique_lock;
//从函数返回一个局部的unique_lock对象是可以的。
//返回这种局部对象,tempUnique_lock会调用unique_lock的移动构造函数
//在下面可以这样调用:
//std::unique_lock
//sbguard1 就有了my_mutex1的所有权
}