同步工具的使用固然能保障我们代码线程安全,但是频繁地使用也会对应用程序的性能造成负面影响。因此,在安全与性能之间权衡是一门经验学问。下面提供了一些应用程序使用同步工具时的技巧。
来源:Threading Programming Guide__Tips for Thread-Safe Designs
1、最好能避免使用同步工具
同步工具确实会影响任何应用程序的性能,如果在设计上使得某个资源会被高度竞争,那么线程等待的时间可能会更长。
在实现并发时,比较优雅的使用方式是减少这些并发任务之间的交互和依赖。如果每个任务都在任务本身的私有数据集上运行,则不需要锁等同步工具去保护这些数据。如果必须要多任务共享单个数据集,可以考虑单独为每个任务拷贝一份副本,当然,使用时得权衡数据拷贝成本和同步工具成本的权衡问题。
2、理解同步工具的局限性
同步工具只有应用程序中所有线程保持统一使用时才体现其效果。举个例子,如果创建了一个互斥锁用于限制对特定资源的访问,那么在操纵资源之前,所有的线程必须获得相同的互斥锁。如果不这样做,会破坏互斥锁所提供的保护。
3、注重代码的逻辑
使用锁和内存屏障时,应该始终考虑它们在代码中的位置。
相关使程序线程安全的示例,可参考:Thread Safety Summary
4、提防死锁和活锁(deadlocks, livelocks)
当线程试图同时获取多个被锁的资源时,就会可能引发死锁。当线程A持有加锁资源a,线程B持有加锁资源b,而它们在释放锁之前都企图获取对方线程上的资源,此时线程A等待线程B释放资源b,线程B等待线程A释放资源a,因此两者会一直在等待(阻塞) 。
产生死锁需要符合的必要条件:
1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
百度百科有一段比较粗俗的描述~:
死锁:迎面开来的汽车A和汽车B过马路,汽车A得到了半条路的资源(满足死锁发生条件1:资源访问是排他性的,我占了路你就不能上来,除非你爬我头上去),汽车B占了汽车A的另外半条路的资源,A想过去必须请求另一半被B占用的道路(死锁发生条件2:必须整条车身的空间才能开过去,我已经占了一半,尼玛另一半的路被B占用了),B若想过去也必须等待A让路,A是辆兰博基尼,B是开奇瑞QQ的屌丝,A素质比较低开窗对B狂骂:快给老子让开,B很生气,你妈逼的,老子就不让(死锁发生条件3:在未使用完资源前,不能被其他线程剥夺),于是两者相互僵持一个都走不了(死锁发生条件4:环路等待条件),而且导致整条道上的后续车辆也走不了。
————————————————————————
活锁:马路中间有条小桥,只能容纳一辆车经过,桥两头开来两辆车A和B,A比较礼貌,示意B先过,B也比较礼貌,示意A先过,结果两人一直谦让谁也过不去 。
5、正确地使用Volatile变量
若当前已使用互斥锁保护代码,无须再设置volatile关键字去保护需要保护的变量。因互斥锁中已经包含一个内存屏障,数据的加载和存储操作的顺序得到了保障。如果再使用volatile修饰锁中变量,会强制每次访问都从内存中加载 ,带来一定的性能损耗,因此可省略volaitle关键字。
同时,对比volatile而言,互斥锁和其他的一些同步机制是保护数据结构完整性的更好的方法,因为volatil关键字仅保障不从寄存器只从内存中加载变量,却不能保障代码是否能正确地访问变量。
参考资料:
1、stackoverflow
2、死锁
3、活锁
4、ios多线程死锁解析
5、维基百科