天舟一号发射国产航母下水C919成功首飞好事一桩接一桩,中国人认定要干的事情没有干不成的实在可喜可贺。这拨沙尘过去了,天真蓝又能大口喘气了真舒服,半夜里骑小蓝车去麦当劳买那么大鸡排,店里竟然坐着一大半客人在用餐,望京国贸CBD金融街上地西二旗写字楼的灯火通明时刻提醒人们这里是繁华的国际大都市,任何不努力都只是借口,什刹海三里屯也灯火通明,算了吧那地少去,喝的不是酒全是寂寞。
做软件开发的总是躲不过同步机制这个话题,并发处理免不了对公共数据进行操作,不认真对待它总找你麻烦,一段代码写的漫天飞舞天花乱坠结果折在同步上是一件很憋屈的事情,一个木桶能盛多少水是由最短的那块木板决定的,没错同步机制处理不好这就是瓶颈。同步机制这个话题有点大,三言两语说不清,需要耐下心慢慢研究,吐血推荐《UNIX网络编程(第2版)》第1卷以及第2卷之进程间通信,作者史蒂文博士,全宇宙知名网络专家教育家,治学严谨而且文章通俗易懂,可惜天妒英才令人扼腕叹息,上帝身边缺个程序猿。没读过史蒂文博士著作的攻城狮不是好码畜,必须精读,必要时动手操练一下受益匪浅绝对一生受用。
同步机制不能不说posix和system v这两种标准,posix比较新方便移植也更标准化一些,在没有竞争条件的情况下,保持用户态调用也更轻量化一些,system v相对老些,每次调用都会进出内核,性能方面略处下风,不追求极致的话哪个都行全凭个人习惯和喜好。主要的同步机制包括锁(自旋锁、互斥锁、条件锁、读写锁)、信号灯(信号量)、记录锁(文件锁),接下来谈谈个人的认识和理解。
先撇清一个问题,有童鞋说原子操作也能实现并发处理对公共数据的排他性操作呀,这话没毛病,原子操作是利用CPU单一指令执行中不可中断的特性实现的,即必须确保对数据的操作在一个指令中完成,显然对一个变量的“读改写”在一条CPU指令里完成是不现实的,因此需要一种机制来保障,由于同步机制考量的是对一系列计算过程或一块代码区间执行的独占性操作,所以这个话题就不在这里讨论了。
自旋锁的意义在于对超短时间内的操作进行轻量级锁定,它会一遍一遍尝试获取锁定,容易造成CPU高占用,并且锁定区间存在阻塞操作的话还将导致潜在的死锁风险,因此对于大块逻辑计算完全不适用,自旋久了容易晕,一晕就容易吐,一吐就容易虚,一虚就容易亏,一亏就需要补~
互斥锁好东西效率真棒没的说,上锁的干活等锁的挂起完全不占用任何计算资源,有两个属性超级有用,一个是PTHREAD_PROCESS_SHARED,把含有该属性的互斥锁放在共享内存中供多个进程分别映射到各自地址空间中,竟然没有比线程共享在效率上有明显衰减,真是了不起;另一个是PTHREAD_MUTEX_RECURSIVE,同一线程下多次锁定不会导致阻塞,在团队开发工作中有时无法彻底杜绝嵌套调用,互斥量就显示出它的价值了,但互斥量必须成对出现,而且也是有系统开销的喔,另外上述两个优惠政策不能同时享受,最终解释权归操作系统。
条件锁跟互斥锁基本一回事只是增加了条件判断,满足才触发下面的流程,否则挂起等待,节省了大量轮询的资源消耗。唤醒方式分单播与广播,在明确条件等待者身份唯一性的情况下单播已经够用了,否则推荐更可靠的广播方式发送,假如对唤醒还是不放心的话还可以调用定时等待,条件满足或超时立即返回,这样够放心了吧。
读写锁是在互斥锁基础上对特定应用场景的优化,写写之间互斥读写之间互斥读读之间共享,总结起来就是数据变动就独占不变就共享,效率比互斥锁差一些也是完全可以理解和接受的。
信号灯(信号量或信号量集合)网上有不少讨论,有认为是一回事只是叫法不同而已,也有认为是二回事,个人支持第二种,因为我们的语文真的是语文老师教的,回到这个严肃的话题上说说区别。灯有两种状态开和关或是亮和灭,非开即关不是亮就是灭,半开半关半亮半灭的灯从来没见过;量代表一个值,可以是十可以是百可以是千,总之不是非此即彼的关系,假设量是一定的,借一个少一个,借光了只能等着,有人还回来了才能再往出借,所以说信号灯是信号量初始为一时的特例,信号量是对信号灯的衍生和扩展,能够解决更加复杂的资源分配问题,长这么大还是第一次被自己清晰的条理和超强的概括能力震撼到。信号灯分两种有名的和无名的,无名比有名快,posix比system v快,无名的用在线程间,放共享内存被映射后进程间可用,有名的原本就为进程间设计的。另外system v还分带不带undo标识的,不带的更快点,带的如果进程over了可以自动释放。两套标准分别提供各自的API,并包含非阻塞的调用方式。
记录锁(文件锁)都是对文件进行操作,区别是粒度不同,记录锁是锁文件区间,文件锁是把对文件的操作当成锁操作并进而转化成独占操作而已,因此锁效率较差用的也越来越少了,但对文件中的部分区间上锁进行读写操作还是有存在和使用的价值的。
下面两张图是尊敬的史蒂文博士对上述各种同步机制在Solaris上测试的结论数据,鉴于上世纪末计算机的计算速度和处理能力,各项数值在当下已经没有实际参考意义,但数值间的差值变化与发展趋势还是能够真实反映各自效率的,具有重大的参考价值。
总结了若干条在使用同步机制时需要注意的事项。
第一:上锁和解锁务必成对出现且尽量放置在相同的调用层级关系中;
第二:坚持谁上锁谁解锁原则,适用在进程、线程、协程乃至于函数和方法;
第三:原则上锁定区间越小越好时间越短越好,兼顾锁定频率适当优化代码;
第四:锁定区间强烈反对任务阻塞操作即便自认为可控也绝不是明智的做法;
第五:进程间同步务必注册回调函数以保证进程或正常或异常终止时能释放锁;
第六:业务流程侧实现锁操作而非在功能算法侧以减少嵌套调用规避死锁风险;
第七:不同应用场景选择适合的同步机制对并发处理效率的提升效果十分明显;
第八:妥善维护好锁属性与状态以及远离非法操作是保证并发处理的重要基础;
总结一下中心思想,对于如何提升并发处理效率最好的思路不是选择具体哪种同步机制,也不是怎么组织优化代码,更不是简单增加进(线)程数,而是压根就不用任何同步机制。之前《高性能服务优化》一文中承诺单聊下同步机制,这篇作业就算是兑现了,文中所涉及的代码实现在http://git.oschina.net/gonglibin/GlbLib-1.0.0中都可以找到,包含封装与测试,需要的自己扒吧。
坚持梦想不忘初心送给今天的自己和各位小伙伴儿,2017年5月8号。