c++程序unordered_map使用lock_guard给线程加独占锁mutex递归锁recursive_mutex的玩法探究


前言

数据库和操作系统中都有提到锁:

对于同一个资源同时进行多线程的更新和读取操作,如果把操作线性化/序列化后前述操作的组合有不同的效果,那么该资源最终状态以及被读取到的状态就是不可控的,这对于严谨性要求严格的程序来说是不可接受的。此时,我们就需要对这个资源加上锁,只有竞争到的才能对该资源进行操作。


一、线程锁是什么?

线程锁常用之一是排他性锁,也就是谁竞争到了(竞争机制可以是时间先后,也可以是优先级先后等),谁就拥有对某个资源或者某些资源的独有操作权限进行读写等,而其他没有竞争到的线程就需要等待该资源释放后的下次竞争。

我们以调用了api的C++程序为例讲讲不加锁的危害及解决办法:

二、unordered_map为什么崩了

1.场景概述

c++程序中使用了第三方提供的api,api推送过来的数据需要做过滤,因为可能是同账号其他客户端的操作产生的。
通过api交互的数据有个统一的主键,我们同样把他作为unordered_map的key值。
c++程序内部自定义一个struct,用于存储unordered_mapkey对应的对象的属性。通过api发送之前将struct存入unordered_map中,作为上述key对应的value。
当api有推送返回后,首先通过主键去unordered_map中找是否有key值的记录。如果有则是我们想要的,更新unordered_map中对应value也就是struct内的属性;如果没有则丢弃本条推送。

2.崩溃场景

某日,c++程序的进程实例在运行中崩溃了,报错信息为segmentation violation,最后崩溃的位置是在api的回调线程的run函数中,但api的运行函数是黑盒不公开的(毕竟第三方也没有义务告知你api内部运行方式),所以也没有办法知道最后确切的崩溃位置。

3.bug排查

排查只能从c++程序内部开始:从log查看,有部分api推送过来,因为在unordered_map找不到key值所以被丢弃,之前是偶发出现,在崩溃前频繁出现。而查询unordered_map的插入记录,这些key值及对应的struct是有被插入到unordered_map中的。

4.原因总结

加了些更详尽的log再次复现上述bug,确定了崩溃位置就是对unordered_map的同时操作导致的。虽然c++程序的实例是一个进程,但本地调用api发送及向unordered_map中插入记录,与api回调查找并更新unordered_map中的属性,是不同的线程在进行操作。
所以崩溃的问题就是不同线程同时对unordered_map进行操作导致的。

三、加锁

1.独占锁mutex与lock、unlock

之前我一直以为,加锁是对某个资源添加一种读写属性,所以抽象出来了对于unordered_map的插入、查找方法及几种重载。
经过查阅资料,发现并不需要这么复杂,只需要如下定义一个mutex全局变量:

std::mutex mtx; //mtx 可以改成你喜欢的其他名字

然后在函数中需要对unordered_map进行操作前的位置加上锁,如下所示:

mtx.lock();

在完成对于unordered_map的操作后对其进行释放,以使其他需要对unordered_map进行操作的线程可以完成对于该资源的申请和占有。代码实例如下所示:

mtx.unlock();

2.递归锁recursive_mutex

由于mutex是排他的,所以当某个函数A对资源加锁后,函数A内调用的函数B如果也需要对同一个资源进行申请时就会不成功,进而出现不想要的结果。
这里我们可以将全局变量mtx类型进行变换,代码如下:

std::recursive_mutex mtx; //mtx 可以改成你喜欢的其他名字

这样,同一线程内,函数A调用的函数B就可以对同一资源进行加锁了。

3.更安全的方法lock_guard

上述我们加锁解锁的方式是调用lock和unlock方法,这些方法需要成对出现,否则会造成不可控的结果。
如果lock之后unlock之前的某一步提前退出了,那么就相当于lock与unlock在本次执行中没有成对出现,这是我们不希望出现的,所以需要考虑程序各个出口进行对应处理,但这样维护起来比较麻烦,需要更多的细心与耐心。
这里更推荐使用lock_guard的方法来替代lock与unlock。代码实例如下:

std::lock_guard<std::recursive_mutex> lcgd(mtx); //lcgd 和 mtx 可以改成你喜欢的其他名字

这样即使是崩溃或者提前退出我们也不用担心资源是否释放了。
代码维护起来也更加省心。

4.加锁结果

加锁之后再次运行并进行长测,崩溃再未发生,同时之前的偶发性找不到key进而丢弃推送的现象也消失了。

5.加锁心得

对于加锁的位置尽可能贴近操作,尽可能局部,尽早释放,这样可以节省出来给其他同样需要该资源的地方,减少整体竞争等待消耗。


总结

unordered_map是个高效的存储方式,但是对于其的读写操作(插入、查找)需要保证数据的安全性。
这时候对资源也就是unordered_map加锁是个比较好的解决方法。
推荐使用组合:递归锁recursive_mutex与lock_guard的组合。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值