19.2.1 需要互斥的例子 在多线程环境中,无论哪个函数或方法都可以在多线程中同时执行。但是,在使用共享变量时, 或者在执行文件输出或绘图等的情况下,多线程同时执行就可能得到奇怪的结果。 例如,使用整数全局变量 totalNumber 来累加所处理的数据的个数。为了执行下面的加法计算, 在多线程环境中执行该方法会得到什么结果呢? -(void)addNumber:(NSIngeger)n { totalNumber+=n; } 在 OS 功能支持下,线程在运行的过程中会时而得到 CPU 的执行权,时而被挂起执行权,2 个 方法的执行情况如图 19-1 中所示。在该图中,线程 1 将新计算的值保存在寄存器时挂起 CPU 执行 权,同时线程 2 开始执行方法。即使 CPU 的执行权被挂起,寄存器的值也仍然可以被保存,所以各 线程都能正常处理。但是,由于线程 2 写入的值消失了,因此整体上看,这偏离了我们期待的结果。 原因是值的读取、更新、写入操作被多线程同时执行了。 在图 19-1 的例子中,我们将同时只可以由一个线程占有并执行的代码部分称为临界区(critical ,或称为危险区。互斥的目的就是限制可以在临界区执行的线程。 19.2.2 锁 为了使多个线程间可以相互排斥地使用全局变量等共享资源,可以使用NSLock类。该类的实例 也就是可以调整多线程行为的信号量(semaphore)或者互斥型信号量(mutualexclusionsemaphore)。 Cocoa 环境中也称为锁(lock)。 锁具有每次只允许单个线程获得并使用的性质。获得锁称为“加锁”,释放锁称为“解锁”。 锁和普通的实例一样,使用类方法alloc和初始化器init来创建并初始化。但是,锁应该在程 序开始在多线程执行前创建。 NSLock*countLock=[[NSLockalloc]init]; 获得锁的方法和释放(unlock)锁的方法都在协议 NSLocking 中定义。 -(void)lock 如果锁正被使用,则线程进入休眠状态。 如果锁没有被使用,则将锁的状态变为正被使用,线程继续执行。 -(void)unlock 将 锁置为没有在被使用,此时如果有等待该锁资源的正在休眠的线程,则将其唤醒。 在上例中,使用锁后会产生如下效果。但需要预先创建 NSLock 的实例 aLock。在该代码中,从 某线程执行 A 取得锁到该线程执行 B 释放锁期间,其他线程在执行 A 时将进入休眠状态,不能执 行临界区代码。锁被释放后,在执行 A 时休眠的线程中选择一个线程,该线程在取得锁后进入临界 区执行。 -(void)addNumber:(NSIngeger)n { [aLocklock]; ───────────────────────────────────────── A totalNumber+=n;//临界区 [aLockunlock];──────────────────────────────────────── B } 某个锁被lock后,必须执行一次unlock。而且lock和unlock必须在同一个线程执行 A。 下面来看另外一个使用锁的例子。考虑一下全局变量值自增时返回其结果的方法。多线程执行 时,全局变量 theCount 若想正确地自增,就需要使用锁 countLock 来管理。 可以采用如下定义。 A lock 和 unlock 必须在同一个线程中执行,因为 NSLock 是基于 POSIX 线程实现的。 -(int)inc { [countLocklock]; ++theCount; [countLockunlock]; returntheCount; } 咋一看好像没问题,但从释放锁到返回值期间,其他线程可能会修改变量值。如下所示。 -(int)inc { inttmp; [countLocklock]; tmp=++theCount; [countLockunlock]; returntmp; } 各线程持有独立的栈,自动变量 tmp 可以在整个线程中局部利用,而且无需担心被其他线程访 问。如果按照这样的方式执行,就可以得到临界区之后的正确值。 19.2.3 死锁 线程和锁的关系必须在设计之初就经过仔细的考虑。如果错误地使用锁,不但不能按照预期执 行互斥,还可能使多个线程陷入到不能执行的状态,即死锁(deadlock)状态。 死锁就是多线程(或进程)永远在等待一个不可能实现的条件而无法继续执行,如图 19-2 所示。 图 19-2 陷入死锁的典型例子 [lockForA lock]; ... /* ၸ͇"ᄉܪူ */ [lockForB lock]; ... /* ၸ͇"֖#ᄉܪူ */ [lockForB unlock]; [lockForA unlock]; ጲር [lockForB lock]; ... /* ၸ͇#ᄉܪူ */ [lockForA lock]; ... /* ၸ͇"֖#ᄉܪူ */ [lockForA unlock]; [lockForB unlock]; ጲር 线程 1 占有文件 A 并正在进行处理,途中又需要占有文件 B。而另一方面,线程 2 占有着文件 B,途中又需要占有文件 A。大家不妨设想一下,如果线程 1 和线程 2 同时执行到了图中的箭头位置 会怎么样呢?线程 1 为了处理文件 B 想要获得锁 lockForB,但是它已经被线程 2 获得。同样,线程 2 想要获得的锁 lockForA 也被线程 1 占有着。这种情况下,线程 1 和线程 2 就会同时进入休眠状态, 而且双方都不能跳出该状态。 像这样,当多个线程互相等待资源的释放时,就非常容易出现死锁现象。有时是多个线程相干预,有时则是一个线程因为自己需要获得锁而进入休眠状态。此外,由于多数情况下各个线程本身 并没有错误处理,而且死锁又随时可能发生,因此追究原因就非常困难,也不能排除导致程序 bug 的可能。 19.2.4 尝试获得锁 NSLock 类不仅能获得锁和释放锁,还有检查是否能获得锁的功能。利用这些功能,就可以在不 能获得锁时进行其他处理。 -(BOOL)tryLock 用 接收器尝试获得某个锁,如果可以取得该锁则返回 YES。不能获得时,与lock处理不同,线程没 有进入休眠状态,而是直接返回 NO 并继续执行。 该方法十分便利,但要确保只能在可以获得锁时才执行 unlock,创建程序时必须注意这一点。 19.2.5 条件锁 。该锁持有整数值,根据该值可以获得锁或者 等待。 -(id)initWithCondition: (NSInteger)condition N SConditionLock 实例初始化,设置参数 condition 指定的值。 NSCondtionLock 的指定初始化器。 -(NSInteger)condition 此时返回锁中设定的值。 -(void)lockWhenCondition:(NSInteger)condition 如果锁正在被使用,则线程进入休眠状态。 锁 不在被使用时,如果锁值和参数 condition 的值一致,则将锁状态修改为正在被使用,然后继续执 行,如果不一致,则线程进入休眠状态。 -(void)unlockWithCondition: (NSInteger)condition 在 锁中设置参数 condition 指定的值。将锁设置为不在被使用,此时如果有等待获得该锁且处于休眠 状态的线程,则将其唤醒。 -(BOOL)tryLockWhenCondition: (NSInteger)condition 尚 未使用锁且锁值与参数 condition 相同时,获得锁并返回 YES。不能获得锁时也不进入休眠状态, 而是返回 NO,线程继续执行。 使用方法lock、unlock或tryLock都 可以获得锁和释放锁,而且无需关心锁的值。 然而,由于 NSConditionLock 实例可以持有的状态为整数型,所以事先用枚举常数或宏定义就可 以了。如果只使用 0 或 1,不仅不容易理解,也可能造成错误。 19.2.6 NSRecursiveLock 某线程获得锁后,到该线程释放锁期间,想要获得该锁的线程就会进入休眠。使用类 NSLock 的 锁时,如果已经获得锁的线程在没有释放它的情况下还想再次获得该锁,该线程也会进入休眠状态。 但是,由于没有从休眠状态唤醒的线程,所以这就是死锁。下面是一个简单的例子,这段代码不会 执行。 [aLocklock]; [aLocklock];//这里发生死锁 [aLockunlock]; [aLockunlock]; 解决这种情况可以使用NSRecursiveLock类的锁,拥有锁的线程即使多次获得同一个锁也不会 进入死锁。但是,其他线程当然也不能获得该锁。获得次数和释放次数一致时,锁就会被释放。 NSRecursiveLock 类的锁使用起来十分方便,但排除被重复加锁的情况,用 NSLock 来重新记述 的话,性能则会更好。 19.2.7 @synchronized 程序内的块可以指定为不被多线程同时使用。为此可以使用 @synchronized 编译符,如下所示。 语法 线程的锁 @synchronized(obj ) { //记述想要排斥地执行的内容 } 通过使用该段代码,运行时系统就可以创建排斥地执行该代码块的锁(mutex)。参数 obj 通常指 定为该互斥锁要保护的对象。obj 自己不需要是锁对象。 线程如果要执行该代码块,首先会尝试获得锁,如果能获得锁则可以执行块内代码。块执行结 束时一并释放锁。使用 break 或 return 从块内跳出到块外时也被视作块执行终止。而且,在块内发生 异常时,运行时系统会捕捉异常并释放块。 @synchronized 的参数对象决定对应的块。所以,同一个对象参数的@synchronized块如果有多 个,则不可以同时执行。 根据参数的选择方法的不同,@synchronized 会在并行执行的受限对象和可以执行的普通对象之 间动态切换。下面展示 @synchronized 参数的使用示例。 (a) 是指定只能单独存在的对象时的情景。同一个对象在其他地方也作为 @synchronized 的参数 使用时,所有这些块不能同时执行。(b) 也是一样,因为限制了参数的使用范围,互斥对象显然只能 是该方法内的块。 (c) 是各个实例互斥的例子。一个实例一次只能执行一个线程,同一类别的其他实例则多个线程可以同时存在。(d) 在参数对象可能在多个地方更改的情况下有效,但以同样方式使用该对象的所有 场所中都需要按照该方式书写,否则就没有任何意义。 而且,也可以按照 (e) 的方式书写。此外还可以指定类对象,或者使用消息选择器(隐藏参数的 _cmd)来指定方法等。不过一般情况下,为互斥的对象使用专门的锁对象是比较可靠的方法。 staticidg=...; -(void)doSomething:(id)arg { staticidloc=...; @synchronized(g){...}//(a) @synchronized(loc){...}//(b) @synchronized(self){...}//(c) @synchronized(arg){...}//(d) @synchronized([selflocalLock]){...}//(e) } @synchronized 与上述 NSRecursiveLock 类的锁一样,可以递归调用。例如,下述这种简单的例 子就不会死锁。 @synchronized(obj){ @synchronized(obj){ ... } } 使用 @synchronized 块时,加锁和解锁必须成对进行,因此可以防止加锁后忘记解锁这种问题的 发生。和普通的锁相比,复杂的并行算法的书写会较为复杂,但多数情况下都会使互斥更容易理解。