死锁也是程序员最多见的问题之一了,可是死锁跟内存泄露不一样,原理和缘由都相对简单,简单说就是你等我,我也等你,就这么耗着!html
但死锁的影响有时比内存泄露更严重。内存泄露主要是渐进式的,可能重启一下就能够从头开始了。而死锁是重启不了,这只是直接影响而已。死锁通常会出现某个功能或者操做无反应,可能进一步没有了心跳而下线,服务中止。而通常的看门狗也发现不了,进程还在。通常都须要手动杀进程。因此对于绝大多数的业务都是不能够接受的。linux
而形成死锁的缘由差异也比较大,有的可能只是程序员的一时疏忽,可有的也会让你头痛。程序员
咱们之前平台的死锁也是屡见不鲜,我记得的常见的有两种状况。session
(一)锁跨度很大,代码的跨度,看上去两个不怎么相关的类,居然在互相调用!还带着锁。我印象中咱们的流媒体出现过的一次死锁,就是有两个TCP session各自的两个函数在嵌套调用。数据结构
(二)一把锁,涉及范围很大,锁定一个对象的操做可能已经有四五种,可是涉及使用到的函数倒是翻倍甚至几十个都有可能。虽然也在一个类里面,可是类很长,带有同一把锁的函数之间就可能出现互相调用。分布式
一看就知道都是设计的问题,不出问题才怪。但是问题要解决啊,针对这些问题,后面我琢磨出了一套方法。函数
案例有点久远,当时没有留下文档,所幸代码还在,针对上面第二种状况的。因此只能是稍微描述下当时的状况和截图看看最后是如何解决的。优化
首先咱们看下这个类有多长:spa
有没有傻眼。这又要勾起我多少痛苦的回忆。也好吧,让大家开心一下。不过大家也开心不了多久,我都有解决之道:)设计
看看我留下的痕迹:
改动了31行,这还只是关于关键字的搜索。有多少个函数,你猜,哈。咱们主要看后面的注释,有两次提到“可能同时”调用或者进来。你也能够看到,个人解决方法是使用了位运算。
这一招又是从上一家学来的。其实如今看不少开源库和内核都是大量使用了位运算,不少文档也提到了,像Redis、文件系统、虚拟内存等。
咱们再来看看定义:
老的锁已经放注释里面的了,锁的对象是一个链表list。新添加了一个整型变量,把变量的几个值定义成一个枚举类型。
因此这几个状况就表明了几种功能,这里是四种状况,但是实现类里面却有31处!你说能不死锁吗?
咱们再次还原下当时的情景。
这个list是文件列表,而它的业务无非是增删改查。若是设计简单的话,一把锁也够了。可是真正简单设计有这么容易吗?
咱们又回到这个类,第一个截图显示2500行,根据设计基本原则,通常一个类不能超过1000行。这里早就能够划分至少三个类了。
怎么划分,有人会建议把这个list单独拿出去,是,我也想过。可是关系复杂了,因此咱们又到了第二张图,你看涉及到的函数只会有增删改查吗?
和其余的对象和方法交织在一块儿了!要想抽丝剥离,只能重构!事实上,后面都重构了。
可是问题要解决。重构是后面的事,一旦出现这种严重问题,当下就是解决问题。因此我后面去掉了锁,重现定义了新的变量。具体怎么弄?
见最后这张图,一个变量四个值,可是这四个值可不是连续的,看到了吗,0、一、二、4,为何?
由于要实现二进制运算,因此他们的的二进制位对应就是,0000、000一、00十、0100。每一个值用一位表示一种操做,互不干扰。该位为1表示占用,若是是0表示未占用。表明了之前的锁状态。
因此虽然锁没有了,可是(锁的)功能仍是有的。这是一个方面,不能影响原有的功能,原来的样子(虽然很差看,可是不能再引起其余问题了)。另外一方面,问题也要解决,仍然是利用了这几个位!
上面的四个值,对应的不彻底是增删改查,具体对应了:初始化、查、删、删而且加四个状态,但实际上操做是后三种。事实上初始化值0也能够说没有占位。
开始咱们提到了每一个位互不干扰,如今肯定是三个位互不干扰。因此在进入某种操做时,首先判断当前状态,是可重入仍是须要等待。
例如说,若是当前只是查,那么继续查(另外一个查操做)确定没问题,而其余两种须要稍微等一下,这里的等待是20次sleep的20ms循环,只要查操做结束,立刻进入下一步。
可是若是循环已经完成,而状态依然没变化,那么这里不等待了,直接退出。下次再进来询问。
因此这里不一样的操做对应了不一样的方式,因状况而异。这样就不会致使死锁。同时,这些改变都须要加日志跟踪,能够发现等待了多久,哪一个函数占用时间太长,若是能减小该函数占用的时间就是最好的了。在实际项目中,能优化的也有。但有的就只能惊讶了,有碰到过一个方法里面有调用两个while嵌套循环,简单的计算也行了,有些循环里面还调用多个方法。因此只能用这种方法了。
固然这个解决方法是有点抽象,因此为了说清楚这个方法,我想了好久,其余部分早写完了,剩下这里反复改,但愿你能看明白。
其实,我后面再看分布式的锁的实现,原理和复杂程度也不过如此:),由于咱们这些代码早就把我给臣服了:(
咱们上面提到了解决方法,那么它的理论依据是什么?
咱们稍微窥视一下锁的实现。linux 2.6 kernel的源码,互斥锁所使用的数据结构:
这里只是列出了内核中,锁的定义,其实它的实现还有不少。有兴趣的能够看源码。咱们回到这个主题,不知你们发现没有,其实锁的本质也是一个整型变量。
而我就是利用了这个特性,固然也有一点自旋锁的特性。你能够再往会看,第二张图,其中有三处for循环,就是说我会根据状况进行判断和等待一会,但不是忙等待,就是说到了必定的时间后,我会强制改变状态和退出。因此和自旋锁又有不一样。
因此总结一下,原理很重要!
和内存泄露同样,死锁的预防也在于设计。因此代码的质量在于设计!这里一样只针对死锁的问题提几个建议。
锁定的代码行数,必定用到的时候才用,只将相关的变量括起来。而不是锁定整个函数。
写段伪代码说明下。
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更好。
一般,一个变量一把锁,或者一个功能点一把锁,而不是一个类一把锁。
那有的人会说若是要锁住一个类,怎么办?
我见过的只有在一种状况下一个类才须要用到锁,就是把这个类当变量使用。因此这种状况也能够概括到一个变量,或者说一个对象。而这种状况通常用在单例模式中,因此即便锁住也不可能出现方法的嵌套而致使死锁。关于单例模式的使用,我后面还有文章将会介绍。很快,后面第二篇吧。
并且这里说的一个变量,或者一个功能点要职责单一。一个类未尝不是如此!
案例里面其实就是函数的功能模糊,类的职责模糊,估计当时都没有设计,反正把相关的都放一块儿,一锅乱炖!
因此这是设计和开发里面的大忌!后面就是改不完的Bug、踩不完的坑。。
尽可能不用锁、少用锁。非用不可才用锁。
一方面由于多了容易形成死锁,另外一方面锁有必定的消耗。上面提到的源码只是一个定义而已,而它的实现不只仅有几处循环,还有回调函数。
固然,这一点提及来容易,作起来难!具体怎么少用,有没有好的方法?
个人回答固然是有,请听下回分解。