zoukankan      html  css  js  c++  java
  • 程序员的踩坑经验总结(四):死锁

    死锁也是程序员最常见的问题之一了,但是死锁跟内存泄露不同,原理和原因都相对简单,简单说就是你等我,我也等你,就这么耗着!

    但死锁的影响有时比内存泄露更严重。内存泄露主要是渐进式的,可能重启一下就可以从头开始了。而死锁是重启不了,这只是直接影响而已。死锁一般会出现某个功能或者操作无反应,可能进一步没有了心跳而下线,服务停止。而一般的看门狗也发现不了,进程还在。一般都需要手动杀进程。所以对于绝大多数的业务都是不可以接受的。

    而造成死锁的原因差别也比较大,有的可能只是程序员的一时疏忽,可有的也会让你头痛。

    我们以前平台的死锁也是家常便饭,我记得的常见的有两种情况。

    (一)锁跨度很大,代码的跨度,看上去两个不怎么相关的类,竟然在互相调用!还带着锁。我印象中我们的流媒体出现过的一次死锁,就是有两个TCP session各自的两个函数在嵌套调用。

    (二)一把锁,涉及范围很大,锁定一个对象的操作可能已经有四五种,但是涉及使用到的函数却是翻倍甚至几十个都有可能。虽然也在一个类里面,但是类很长,带有同一把锁的函数之间就可能出现互相调用。

    一看就知道都是设计的问题,不出问题才怪。可是问题要解决啊,针对这些问题,后面我琢磨出了一套方法。

     

    案例分析

    案例有点久远,当时没有留下文档,所幸代码还在,针对上面第二种情况的。所以只能是稍微描述下当时的情况和截图看看最后是如何解决的。

    首先我们看下这个类有多长:

        

    有没有傻眼。这又要勾起我多少痛苦的回忆。也好吧,让你们开心一下。不过你们也开心不了多久,我都有解决之道:)

    看看我留下的痕迹:

         

    改动了31行,这还只是关于关键字的搜索。有多少个函数,你猜,哈。我们主要看后面的注释,有两次提到“可能同时”调用或者进来。你也可以看到,我的解决方法是使用了位运算

    这一招又是从上一家学来的。其实现在看很多开源库和内核都是大量使用了位运算,很多文档也提到了,像Redis文件系统虚拟内存等。

    我们再来看看定义:

        

    老的锁已经放注释里面的了,锁的对象是一个链表list。新添加了一个整型变量,把变量的几个值定义成一个枚举类型。

    所以这几个情况就代表了几种功能,这里是四种情况,可是实现类里面却有31处!你说能不死锁吗?

     

    我们再次还原下当时的情景。

    这个list是文件列表,而它的业务无非是增删改查。如果设计简单的话,一把锁也够了。但是真正简单设计有这么容易吗?

    我们又回到这个类,第一个截图显示2500行,根据设计基本原则,一般一个类不能超过1000行。这里早就可以划分至少三个类了。

    怎么划分,有人会建议把这个list单独拿出去,是,我也想过。但是关系复杂了,所以我们又到了第二张图,你看涉及到的函数只会有增删改查吗?

    和其他的对象和方法交织在一起了!要想抽丝剥离,只能重构!事实上,后面都重构了。

    但是问题要解决。重构是后面的事,一旦出现这种严重问题,当下就是解决问题。所以我后面去掉了锁,重现定义了新的变量。具体怎么弄? 

    见最后这张图,一个变量四个值,但是这四个值可不是连续的,看到了吗,0、1、2、4,为什么?

    因为要实现二进制运算,所以他们的的二进制位对应就是,0000、0001、0010、0100。每个值用一位表示一种操作,互不干扰。该位为1表示占用,如果是0表示未占用。代表了以前的锁状态。

    所以虽然锁没有了,但是(锁的)功能还是有的。这是一个方面,不能影响原有的功能,原来的样子(虽然不好看,但是不能再引发其他问题了)。另一方面,问题也要解决,仍然是利用了这几个位!

    上面的四个值,对应的不完全是增删改查,具体对应了:初始化、查、删、删并且加四个状态,但实际上操作是后三种。事实上初始化值0也可以说没有占位。

    开始我们提到了每个位互不干扰,现在确定是三个位互不干扰。所以在进入某种操作时,首先判断当前状态,是可重入还是需要等待

    例如说,如果当前只是查,那么继续查(另一个查操作)肯定没问题,而其他两种需要稍微等一下,这里的等待是20次sleep的20ms循环,只要查操作结束,马上进入下一步。

    但是如果循环已经完成,而状态依然没变化,那么这里不等待了,直接退出。下次再进来询问。

    所以这里不同的操作对应了不同的方式,因情况而异。这样就不会导致死锁。同时,这些改变都需要加日志跟踪,可以发现等待了多久,哪个函数占用时间太长,如果能减少该函数占用的时间就是最好的了。在实际项目中,能优化的也有。但有的就只能惊讶了,有碰到过一个方法里面有调用两个while嵌套循环,简单的计算也行了,有些循环里面还调用多个方法。所以只能用这种方法了。

     

    当然这个解决方法是有点抽象,所以为了说清楚这个方法,我想了很久,其他部分早写完了,剩下这里反复改,希望你能看明白。

    其实,我后面再看分布式的锁的实现,原理和复杂程度也不过如此:),因为我们这些代码早就把我给臣服了:(

    总结和建议

    (一)原理与依据

    我们上面提到了解决方法,那么它的理论依据是什么?

    我们稍微窥视一下锁的实现。linux 2.6 kernel的源码,互斥锁所使用的数据结构:

    这里只是列出了内核中,锁的定义,其实它的实现还有很多。有兴趣的可以看源码。我们回到这个主题,不知大家发现没有,其实锁的本质也是一个整型变量

    而我就是利用了这个特性,当然也有一点自旋锁的特性。你可以再往会看,第二张图,其中有三处for循环,就是说我会根据情况进行判断和等待一会,但不是忙等待,就是说到了一定的时间后,我会强制改变状态和退出。所以和自旋锁又有不同

    所以总结一下,原理很重要!

    (二)死锁的预防

    和内存泄露一样,死锁的预防也在于设计。所以代码的质量在于设计!这里同样只针对死锁的问题提几个建议。

    1.减少锁定代码的范围

    锁定的代码行数,一定用到的时候才用,只将相关的变量括起来。而不是锁定整个函数。

    写段伪代码说明下。

    std::mutex  m_mutex;
    
    int  g_diff = 3;
    
    int funA()
    
    {
    
    unique_lock<mutex> lock(m_mutex);
    
    int a = 5;
    
    //中间省去若干
    
    return a+g_diff; 
    
    } 
    
    int funB()
    
    {
    
    int a = 5;
    
    int b = 0;
    
      {
    
        unique_lock<mutex> lock(m_mutex);
    
        b = a+g_diff; 
    
      }
    
    //中间省去若干
    
    return b;
    
    } 

    函数funB肯定比函数funA更好。

    2.降低锁的粒度

    通常,一个变量一把锁,或者一个功能点一把锁,而不是一个类一把锁。

    那有的人会说如果要锁住一个类,怎么办?

    我见过的只有在一种情况下一个类才需要用到锁,就是把这个类当变量使用。所以这种情况也可以归纳到一个变量,或者说一个对象。而这种情况一般用在单例模式中,所以即使锁住也不可能出现方法的嵌套而导致死锁。关于单例模式的使用,我后面还有文章将会介绍。很快,后面第二篇吧。

    而且这里说的一个变量,或者一个功能点要职责单一。一个类何尝不是如此!

    案例里面其实就是函数的功能模糊,类的职责模糊,估计当时都没有设计,反正把相关的都放一起,一锅乱炖!

    所以这是设计和开发里面的大忌!后面就是改不完的Bug、踩不完的坑。。

    3.减少锁的使用

    尽量不用锁、少用锁。非用不可才用锁。

    一方面因为多了容易造成死锁,另一方面锁有一定的消耗。上面提到的源码只是一个定义而已,而它的实现不仅仅有几处循环,还有回调函数。

    当然,这一点说起来容易,做起来难!具体怎么少用,有没有好的方法?

    我的回答当然是有,请听下回分解。

     

  • 相关阅读:
    Strom在本地运行调试出现的错误
    能否通过六面照片构建3D模型?比如人脸,全身的多角度照片,生成3D模型。?
    怎么识别自己的眼型?眼型图片参照
    用opencv检测人眼并定位瞳孔位置
    仿射变换
    二维图像的三角形变换算法解释
    Labeled Faces in the Wild 人脸识别数据集
    【图像处理】计算Haar特征个数
    人脸识别技术大总结(1):Face Detection & Alignment
    基于Policy Gradient实现CartPole
  • 原文地址:https://www.cnblogs.com/orange-CC/p/12929766.html
Copyright © 2011-2022 走看看