参考书籍 《多线程编程指南》
原著:Apple Inc.
翻译:謝業蘭 【老狼】
目录
应用程序里面多个线程的存在引发了多个执行线程安全访问资源的潜在问题。两 个线程同时修改同一资源有可能以意想不到的方式互相干扰。
但涉及到线程安全时,一个好的设计是最好的保护。避免共享资源,并尽量减少 线程间的相互作用,这样可以让它们减少互相的干扰。
同步工具
-
原子操作
-
内存屏障和 Volatile 变量
-
锁
锁是最常用的同步工具。你可以是使用锁来保护临界区(critical section),这 些代码段在同一个时间只能允许被一个线程访问。
Mutex 互斥锁
Recursive lock 递归锁
Read-write lock 读写锁
Distributed lock 分布锁
Spin lock 自旋锁
Double-checked lock 双重检查锁
注意:大部分锁类型都合并了内存屏障来确保在进入临界区之前它前面的加载和存储指令都已经 完成。 -
条件
条件是信号量的另外一个形式,它允许在条件为真的时候线程间互相发送信号。 条件通常被使用来说明资源可用性,或用来确保任务以特定的顺序执行。当一个线程 测试一个条件时,它会被阻塞直到条件为真。它会一直阻塞直到其他线程显式的修改信号量的状态。条件和互斥锁(mutex lock)的区别在于多个线程被允许同时访问一个 条件。条件更多是允许不同线程根据一些指定的标准通过的守门人。
一个方式是你使用条件来管理挂起事件的池。事件队列可能使用条件变量来给等 待线程发送信号,此时它们在事件队列中的时候。如果一个事件到达时,队列将给条 件发送合适信号。如果一个线程已经处于等待,它会被唤醒,届时它将会取出事件并 处理它。如果两个事件到达队列的时间大致相同,队列将会发送两次信号唤醒两个线 程。 -
执行Selector例程
Cocoa 程序包含了一个在一个线程以同步的方式传递消息的方便方法。NSObject 类声明方法来在应用的一个活动线程上面执行 selector 的方法。这些方法允许你的 线程以异步的方式来传递消息,以确保它们在同一个线程上面执行是同步的。比如, 你可以通过执行 selector 消息来把一个从你分布计算的结果传递给你的应用的主线 程或其他目标线程。每个执行 selector 的请求都会被放入一个目标线程的 run loop 的队列里面,然后请求会按照它们到达的顺序被目标线程有序的处理。
同步的成本和性能
同步帮助确保你代码的正确性,但同时将会牺牲部分性能。
线程安全和信号量
在单线程应用程序里面,所有的信号量处理都在主线程进行。在多线程应用程序 里面,信号量被传递到恰好运行的线程,而不依赖于特定的硬件错误(比如非法指令)。 如果多个线程同时运行,信号量被传递到任何一个系统挑选的线程。换而言之,信号 量可以传递给你应用的任何线程。
线程安全设计的技巧
同步工具是让你代码安全的有用方法,但是它们并非灵丹妙药。使用太多锁和其 他同步的类型原语和非多线程相比明显会降低你应用的线程性能。在性能和安全之间 寻找平衡是一门需要经验的艺术。
-
应当避免同步
对于你新的项目,甚至已有项目,设计你的代码和数据结构来避免使用同步是一个很好的解决办法。
实现并发最好的方法是减少你并发任务之间的交互和相互依赖。如果每个任务在它自己的数据集上面操作,那它不需要使用锁来保护这些数据。 -
了解同步的限制
同步工具只有当它们被用在应用程序中的所有线程是一致时才是有效的。如果你创建了互斥锁来限制特定资源的访问,你所有线程都必须在试图操纵资源前获得同一互斥锁。如果不这样做导致破坏一个互斥锁提供的保护,这是编程的错误。 -
注意对代码正确性的威胁
举例
//数组为可变数组,数组内对象为不可变对象
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
//其它线程访问可能会释放anObject 导致[anObject doSomething];访问非法内存;
//所以此处做一次retain处理
[anObject retain];
[arrayLock unlock];
[anObject doSomething];
[anObject release];
4.当心死锁(Deadlocks)和活锁(Livelocks)
任何时候线程试图同时获得多于一个锁,都有可能引发潜在的死锁。当两个不同 的线程分别保持一个锁(而该锁是另外一个线程需要的)又试图获得另外线程保持的 锁时就会发生死锁。结果是每个线程都会进入持久性阻塞状态,因为它永远不可能获 得另外那个锁。
一个活锁和死锁类似,当两个线程竞争同一个资源的时候就可能发生活锁。在发 生活锁的情况里,一个线程放弃它的第一个锁并试图获得第二个锁。一旦它获得第二个锁,它返回并试图再次获得一个锁。线程就会被锁起来,因为它花费所有的时间来 释放一个锁,并试图获取其他锁,而不做实际的工作。
避免死锁和活锁的最好方法是同一个时间只拥有一个锁。如果你必须在同一时间 获取多于一个锁,你应该确保其他线程没有做类似的事情。
5.正确使用Volatile变量
如果单独使用互斥锁已经可以 保护变量,那么忽略关键字 volatile。
通常情况下,互斥锁 和其他同步机制是比 volatile 变量更好的方式来保护数据结构的完整性。
使用原子操作
尽管锁是同步两个线程的很好方式,获取一个锁是一个很昂贵的操作,即使在无竞争的状态下。相比,许多原子操作花费很少的时间来完成操作也可以达到和锁一样的效果。
在多线程情况下,你应该总是使用原子操作,它和内存屏障组合使用来保证多个线程间正确的同步内存。
行者常至,为者常成!