同步工具在保障代码线程安全方面是行之有效方法,但并不是说就是灵丹妙药了。与非多线程性能相比,使用过多的锁或者其他同步方法会明显的降低程序的线程性能。在安全和性能中间找到一个平衡点这就需要经验的积累了。

接下来将会给出在选用同步工具中给出一些建议。

4.4.1 避免同步

不管是一个新工程还是一个已经开发了一段时间的工程,避免同步的最好解决办法就是设计一段好的代码和数据结构。尽管锁或者其他同步工具很有用,但是它们对于程序性能有一定的影响。如果总体设计导致对特定的资源争夺较高,那么线程就有可能会长时间等待。

并发任务的最好的实现办法就是减少交互和相互依赖。如果每一个任务都是在它私有的数据集上面操作,这种情况就不需要使用锁来保护数据。就算是两个任务同时使用同一份数据集,我们可以让该数据集进行分区使用,或者为每一个任务复制一份数据。当然,复制数据也是有消耗的,所以就得衡量一下谁的消耗更高。

4.4.2 理解同步的限制

同步工具在程序中所有线程一起使用时更加有效。如果对特定资源创建一个互斥锁,所有的线程在操作该资源之前都必须要获取该锁。

4.4.3 需要知道线程对代码正确性的威胁

当使用锁或者内存屏障时,我们需要仔细的思考它们在程序中的位置。即使是看起来被放在正确位置的锁,实际上这可能会麻痹你,误认为当前是处于安全的场景。接下来的几个例子尝试去说明一些问题,在看似正确的代码中指出其中的缺陷。前提是要有一个可变的数组中包含一组不可变的对象。假设要调用数组中的第一个对象时,如下代码:

/// 官方文档例子
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[arrayLock unlock];
[anObject doSomething];

在上面这个例子中有一个问题,在释放掉锁以后并在执行doSomething之前,其他线程进来并移除掉数组中所有的元素会发生什么?如果在没有垃圾回收的程序中,持有的该对象就可能会被释放,anObject不会指向有效的内存地址。为了解决这个bug,进行如下修改:

/// 官方文档例子 ,针对doSomething执行段时间任务的处理办法
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;

[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject doSomething];
[arrayLock unlock];

doSomething放进lock里面时,持有的该对象在调用时依然有效。不幸的是,如果doSomething执行的长时间任务的话,这会让代码被长时间锁定,并造成程序的性能瓶颈。

出现该问题时,并不是因为在创建变量时没有正确的设置变量的有效区域,而是由于其他线程触发的内存管理问题。由于它可以被其他线程释放掉,一个更好解决办法是在锁被释放之前对anObject执行retain操作。该方法是解决的变量释放问题,并不会引起性能消耗。

/// 官方文档例子,在arc下就不需要这么做了。
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;

[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject retain];
[arrayLock unlock];

[anObject doSomething];
[anObject release];

尽管上诉例子本质上很简单,但的确阐述了很重要的问题。内存管理等方法也有可能受到多线程环境的影响。此外,我们应该要考虑到编译器在安全方面可能会做的很糟糕。在编码的时候保持这种想法和警惕可以帮我们避免很多潜在的问题,并确保代码的正确性。

4.4.4 注意死锁和活动锁

很多时候一个线程同时会与多个线程交互,这就会潜在的造成死锁问题。死锁造成的原因是当两个线程都持有一个锁,其中一个线程需要这个锁并试着去获取它,然而这个锁却被另一个线程持有。由于它们没办法去获取那个锁,结果就导致每一个线程都会永久被阻塞,

活动锁和死锁有点类似,都是发生在两个线程竞争同一组资源时。在活动锁的场景中,一个线程释放它的第一个锁并尝试去获取第二个锁。一旦它获取到了第二个锁,它又试着返回回去获取第一个锁。它会因为在释放锁和获取锁的时候花费大量的时间,而不是把时间用在正常的工作上。

在解决这两个问题最好的办法就是在同一时间只使用一个锁。如果非要获取多个锁的话,就需要很小心去做相关的操作。

4.4.5 正确使用Volatile变量

如果我们在代码中已经使用互斥锁来保障代码的安全性的时候,不要自己怎么想的就去使用volatile关键字来修饰该变量。互斥锁在包括了内存屏障,以确保在加载和保存变量时能够按照正确的顺序执行。在用volatile关键字来修饰变量时,会在代码的关键部分直接从内存中获取值。在特定情况下,两种同步技术可以组合使用,但是有可能会造成性能损失。如果只需要使用互斥锁时,就不要使用volatile关键字。

反过来,千万不要为了使用volatile关键字,而放弃使用互斥锁。通常来说,互斥锁和其他同步工具保护数据是比使用volatile关键字更好的办法。使用volatile关键字的目的仅仅是保证变量是从内存中加载,而不是经过编译器优化的寄存器中加载。具体关于volatile关键字见第一节“同步工具”一节。

本人总结TIPS

  • 并发任务的最好的实现办法就是减少交互和相互依赖;

  • 在编码的时候保持这种想法和警惕可以帮我们避免很多潜在的问题,并确保代码的正确性;

  • 在解决这两个问题最好的办法就是在同一时间只使用一个锁;

  • 如果只需要使用互斥锁时,就不要使用volatile关键字;

  • 使用volatile关键字的目的仅仅是保证变量是从内存中加载,而不是经过编译器优化的寄存器中加载;

results matching ""

    No results matching ""