当前位置: 首页 > news >正文

锁使用实践经验总结

关于锁的使用,根据我的经验总结如下几点:

减少锁的使用

实际开发中能不使用锁尽量不使用锁,当然这不是绝对的,如果使用锁也能满足性能要求,使用锁也无妨,一般使用了锁的代码会带来如下性能损失:

  • 加锁和解锁操作,本身有一定的开销;
  • 临界区的代码不能并发执行;
  • 进入临界区的次数过于频繁,线程之间对临界区的争夺太过激烈,若线程竞争互斥量失败,就会陷入阻塞,让出 CPU,所以执行上下文切换的次数要远远多于不使用互斥量的版本。

替代锁的方式有很多,如无锁队列。

明确锁的范围

看下面这段代码:

if(hashtable.is_empty()) { pthread_mutex_lock(&mutex); htable_insert(hashtable, &elem); pthread_mutex_unlock(&mutex); }

读者能看出这段代码的问题吗?代码行4虽然对hashtable的插入使用了锁做保护,但是判断hash_table是否为空也需要使用锁保护,所以正确的写法应该是:

pthread_mutex_lock(&mutex); if(hashtable.is_empty()) { htable_insert(hashtable, &elem); } pthread_mutex_unlock(&mutex);
减少锁的粒度

所谓减小锁使用粒度指的是尽量减小锁作用的临界区代码范围,临界区的代码范围越小,多个线程排队进入临界区的时间就会越短。这就类似高速公路上堵车,如果堵车的路段越长,那么后续过来的车辆通行等待时间就会越长。

我们来看两个具体的例子:

示例一

void TaskPool::addTask(Task* task) { std::lock_guard<std::mutex> guard(m_mutexList); std::shared_ptr<Task> spTask; spTask.reset(task); m_taskList.push_back(spTask); m_cv.notify_one(); }

上述代码中guard锁保护m_taskList,仔细分析下这段代码发现,代码行458行其实没必要作为临界区内的代码的,所以建议挪到临界区外面去,修改如下:

void TaskPool::addTask(Task* task) { std::shared_ptr<Task> spTask; spTask.reset(task); { std::lock_guard<std::mutex> guard(m_mutexList); m_taskList.push_back(spTask); } m_cv.notify_one(); }

修改之后,guard锁的作用范围就是78两行了,仅对 **m_taskList.push_back() **操作做保护,这样锁的粒度就变小了。

示例二

void EventLoop::doPendingFunctors() { std::unique_lock<std::mutex> lock(mutex_); for (size_t i = 0; i < pendingFunctors_.size(); ++i) { pendingFunctors_[i](); } }

上述代码中pendingFunctors_是被锁保护的对象,它的类型是std::vector<Functor>,这样的代码效率比较低,必须等当前线程挨个处理完pendingFunctors_中的元素后其他线程才能操作pendingFunctors_。修改代码如下:

void EventLoop::doPendingFunctors() { std::vector<Functor> functors; { std::unique_lock<std::mutex> lock(mutex_); functors.swap(pendingFunctors_); } for (size_t i = 0; i < functors.size(); ++i) { functors[i](); } }

修改之后的代码使用了一个局部变量functors,然后把pendingFunctors_中的内容倒换到functors中,这样就可以释放锁了,允许其他线程操作pendingFunctors_,现在只要继续操作本地对象functors就可以了,提高了效率。

避免死锁的一些建议
  • **一个函数中,如果有一个加锁操作,那么一定要记得在函数退出时记得解锁,且每个退出路径上都不要忘记解锁路径。**例如:

    void some_func() { //加锁代码 if (条件1) { //其他代码 //解锁代码 return; } else { //其他代码 //解锁代码 return; } if (条件2) { if (条件3) { //其他代码 //解锁代码 return; } if (条件4) { //其他代码 //解锁代码 return; } } while (条件5) { if (条件6) { //其他代码 //解锁代码 return; } } }

    上述函数中每个逻辑出口处都需要写上解锁代码。前面也说过,这种逻辑非常容易因为疏忽忘记在某个地方加上解锁代码而造成死锁,所以一般建议使用 RAII 技术将加锁和解锁代码封装起来

  • 线程退出时一定要及时释放其持有的锁

    实际开发中会因一些特殊需求创建一些临时线程,这些线程执行完相应的任务后就会退出。对于这类线程,如果其持有了锁,一定记得在线程退出时记得释放其持有的锁对象。

  • 多线程请求锁的方向要一致,以避免死锁

    假设现在有两个锁 A 和 B,线程 1 在请求了锁 A 之后再请求 B,线程 2 在请求了锁 B 后再请求锁 A,这种线程请求锁的方向就不一致了,线程 1 的方向是从 A 到 B,线程 2 的方向是从 B 到 A,多个线程请求锁的方向不一致容易造成死锁。所以建议的方式是 线程 1 和 线程 2 请求锁的方向保持一致,要么都从 A 到 B,要么都从 B 到 A。

  • 当需要同一个线程重复请求一个锁时,搞清楚你所使用的锁的行为,是递增锁引用计数,还是会阻塞抑或是直接获得锁?

避免活锁的一些建议

前面说了避免“死锁”,读者应该能理解,但是这里突然出现了避免“活锁”,我相信很多人看到这个标题一下子就懵了。所谓活锁就是,当多个线程使用trylock系列的函数时,由于多个线程相互谦让,导致即使在某段时间内锁资源是可用的,也可能导致需要锁的线程拿不到锁。举个生活中的例子,马路上两个人迎面走来,两个人同时往一个方向避让,原来本意是给对方让路,结果还是发生了碰撞。

我们在实际编码时,尽量避免不要过多的线程使用trylock请求锁,以免出现“活锁”现象,这是对资源的一种浪费。

http://www.jsqmd.com/news/1109103/

相关文章:

  • 终极网盘直链下载助手:8大平台一键获取真实下载地址的完整指南
  • SSH连接协议简介
  • SPI接口与74HC165实现高效IO扩展方案
  • 软考机考时间管理实战指南(含真题倒计时模拟表):从“总超时”到“提前5分钟交卷”的逆袭路径
  • AI 加 Web3 应用设计:先把信任边界画清楚
  • 工业4-20mA电流环接收器设计与STM32接口优化
  • 系统性AI应用:从数据契约到模型行为的工业落地实践
  • GTA5线上小助手:终极免费开源工具,开启你的洛圣都自由之旅 [特殊字符][特殊字符]
  • Java编程内功-数据结构与算法「基数排序」
  • Kali Linux渗透测试:从Nmap到OpenVAS的漏洞扫描实战与工作流构建
  • 基于Si4731与MKV58的嵌入式AM/FM收音机开发指南
  • 基于Si4732与MKV58的高性能收音系统设计与优化
  • 圆偏振光 vs 普通膜:从光学原理看屏幕护眼的底层逻辑——悟赫德护景贴观复盾的技术参照
  • 【Springboot毕设全套源码+文档】基于Java+springboot高校学生心理健康管理系统的设计与实现(丰富项目+远程调试+讲解+定制)
  • MAA明日方舟智能辅助工具终极指南:5分钟实现游戏自动化解放
  • TranslucentTB终极指南:10分钟让你的Windows任务栏焕然一新
  • 计算机毕业设计之基于机器学习的英国机场天气状况的分析与预测
  • 从装修风格出发,评估一站式建材服务的适用场景
  • XiaoMusic:让小爱音箱秒变私人音乐管家,无限音乐自由播放
  • 影刀RPA新手教程:请求头Header完全指南——为什么请求被拒绝以及怎么解决
  • 性能测试实战:从并发量计算到工具指标解读的完整指南
  • TranslucentTB完整配置教程:打造Windows任务栏透明化终极方案
  • 机器学习模型生产化:从Notebook到稳定在线服务的工程实践
  • STM32与74HC32实现高效2x2键盘矩阵方案
  • STM32H743ZI与DC-DC降压电源设计实战
  • MAA明日方舟助手终极指南:3步解放双手,轻松搞定日常任务
  • 科研作图告别熬夜!paperxie AI 科研绘图分栏式操作,新手也能秒出期刊规范图
  • 从零开始合法挖洞:白帽子实战指南与漏洞盒子平台解析
  • 智慧工会:当职工服务遇上“数智大脑”
  • Translumo:3步搞定Windows游戏视频实时翻译,新手也能轻松上手