多线程应用程序的存在,在运行打开一个潜在的多线程安全的接入资源。
两个线程相同的资源可能会以意想不到的方式改变相互干扰。
例如。一个线程可以覆盖有一个线程改变或使应用程序进入一个潜在的无效的状态未知。
假设你幸运,毁坏的资源也能导致明显的性能问题或相对easy追踪和修复的崩溃。假设你不幸,然而。毁坏的资源可能导致微妙的错误,一直不显现直到非常久以后,或者错误可能须要对底层编码设计进行彻底检查。
当涉及到线程安全时,好的设计是最好的保护。避免资源共享和降低线程间的交互使它们不太相互干扰。
一个全然抗干扰的设计并不存在。然而。
线程必须交互的情况下,你须要使用同步工具以确保当它们相互作用是他们这样做是安全的。
OSX和iOS提供大量的同步工具。延伸到提供相互排斥訪问应用中序列事件的工具。下面章节描写叙述这些工具以及怎样在你的代码中使用它们来安全訪问程序中的资源。
同步工具
为了防止不同线程意外的更改数据,你能够设计你的应用没有同步问题或者你能够使用同步工具。虽然避免同步问题是全然可取的,这并不总是可能。下面章节描写叙述供你使用的同步工具的基本类别。
原子操作
原子操作是同步的一种简单形式。用于简单数据类型。
原子操作的长处是他们不堵塞竞争线程。对于简单的操作。比如添加计时器变量,比锁这会有更好的性能。
OSX和iOS包括很多操作来运行32位和64位值的基本数学和逻辑操作。这些操作是比較-交换、測试-设置、測试-清除操作的原子版本号。关于支持的原子操作的列表,见 /usr/include/libkern/OSAtomic.h
头文件或原子(atomic)手冊页。
内存屏障和不稳定变量
为了实现最优性能。编译器经常又一次排序汇编级别指令来尽可能保持处理器指令管道完整。
作为这样的优化的一部分,编译器可能又一次排序指令。当它觉得这样做不会产生不对的数据,这些指令会訪问主要内存。不幸的是。检測全部依赖内存的操作对编译器来说不可能。假设看似独立的变量相互影响,编译器优化可能以错误的顺序更新这些变量,产生不对的结果。
内存屏障是一种非堵塞同步工具用来确保内存操作以正确的顺序发生。内存屏障就像一个栅栏,迫使处理器完毕不论什么在栅栏前面的载入和存储操作。然后才同意运行栅栏后面的载入和存储操作。内存屏障通经常使用于确保线程(但看上去是还有一个线程)的内存操作以预期的顺序发生。在这样的情况下没有内存屏障可能让其它线程看到貌似不可能的结果。(比如,见*的内存屏障(memorybarriers)条目。)为了使用内存屏障,你仅仅需在你代码适当的位置调用OSMemoryBarrier
函数。
不稳定变量应用还有一种类型的内存来约束独立变量。
编译器通常通过载入变量值到寄存器来优化代码。对于局部变量。这通常不是一个问题。
然而假设该变量对还有一个线程是可见的,这种优化可能会阻止其它线程注意该值的变化。变量使用volatile
keyword,每次使用该变量时。将强制编译器从内存中载入该变量。假设变量的值可能在不论什么时候被外部来源改变。且编译器无法检測到,你能够声明一个变量为volatile
。
由于内存屏障和不稳定变量降低编译器可运行的优化。应该慎重使用它们并仅仅在须要的地方使用以确保正确性。关于使用内存屏障的很多其它信息。參见OSMemoryBarrier 手冊页。
锁
锁是最经常使用的同步工具之中的一个。你能够使用锁来保护你代码的关键部分。这段代码仅仅同意一个线程訪问。比如,一个关键部分可能操作特定数据结构或使用一些最多一次支持一个client的资源。
通过这章的锁,你能够排除其它线程进行影响代码正确性的更改。
表4-1 列出了程序猿经常使用的一些锁。OS X和iOS提供大部分类型锁的实现。但不是所有。
对于不支持锁类型,说明列解释了这些锁在平台上不直接实现的原因。
表4-1 锁类型 |
|
锁 |
描写叙述 |
Mutex 相互排斥锁 |
一个相互排斥锁(或相互排斥)作为保护资源的一个屏障。相互排斥锁是一种信号,一次仅仅授予一个线程訪问权限。假设使用相互排斥锁而且还有一个线程试图获取它,该线程堵塞直到相互排斥锁被原持有人释放。 假设多个线程竞争同一相互排斥锁。一次仅仅同意一个訪问。 |
Recursive lock 递归锁 |
递归锁是相互排斥锁的一个变体。递归锁同意一个线程在锁释放前多次获取锁。其它线程堵塞直到锁的全部者多次释放锁,且释放锁的次数与获取锁的次数一样。递归锁主要在递归迭代中使用。但也能够在多个方法须要分别获取锁的情况下使用。 |
Read-write lock 读写锁 |
读写锁也称为共享-专有锁。 这样的类型的锁通经常使用于大规模操作。假设仅仅是保护频繁读取但偶尔改动的数据结构,能够极大的提高性能。 在正常操作期间,多个读取者能够同一时候訪问数据结构。当一个线程想要改动这个结构。可是,它堵塞直到全部读取者释放该锁,此时它获取锁并更新数据结构。而当一个写入线程等待锁。新的读取线程堵塞直到写入线程完毕。系统仅支持使用POSIX线程的读写锁。 关于怎样使用这些锁的很多其它信息,參见pthread 手冊页。 |
Distributed lock 分布式锁 |
分布式锁提供了在进程级别的相互排斥訪问。 与真正的相互排斥锁不同。分布式锁不堵塞进程或阻止进程执行。 当锁忙时。它仅仅是简单的报告并让进程决定怎样继续。 |
Spin lock 自旋锁 |
自旋锁多次轮询自己锁条件直到条件变为真。自旋锁最经常使用于多处理器系统,预期等待锁的时间非常小。在这些情况下,相比于堵塞线程,它通常能够更高效的轮询,包含环境切换和线程数据结构的更新。系统不提供不论什么自旋锁的实现。由于它们的轮询性质,但在特定情况下能够非常easy的实现它们。关于内核中实现自旋锁的信息,參见内核编程指南(Kernel Programming Guide)。 |
Double-checked lock 双重检查锁 |
双重检查锁试图通过在採用锁之前測试锁来减少锁的开销。 由于双重检查锁可能不安全。系统不显式的提供支持,不鼓舞使用。 |
注意:大多数类型的锁包含内存屏障确保载入和存储指令在进入关键部分之前完毕。
关于怎样使用锁。见使用锁(Using
Locks)。
条件
条件是还有一种类型的信号。同意线程在某个特定条件是真的时候互相发信号。条件通经常使用来表示一个资源的可用性或者确保任务依照特定的顺序运行。当一个线程測试条件时,线程堵塞除非条件变为真。它仍然堵塞直到其它线程显式的更改并发送条件。条件和相互排斥锁的差别是多个线程能够同一时候訪问条件。条件更加像看门人。依据一些特定的标准让不同线程通过门。
你能够使用条件的一个方法是管理等待事件池。当队列中有事件,事件队列使用一个条件变量来发送信号给等待线程。
假设事件到达,队列将适当的给条件发送信号。假设线程已经在等待。它会醒来并将事件放到队列中并处理它。
假设两个事件在大致同样的时间进入队列。队列将两次发送信号给条件来唤醒两个线程。
系统以几种不同的技术提供条件支持。
条件的正确实现须要细致编码,然而。在代码中使用条件前你应该看看使用条件(Using
Conditions)中的样例。
运行选择器程序
Cocoa应用有一个方便的方法以同步的方式来传递消息到一个线程。
NSObject 类声明的方法来运行应用活动线程的选择器。
这些方法让你的线程异步的交付消息保证目标线程同步运行这些消息。比如。你能够使用运行选择器消息来交付分布式计算的结果到你的应用主线程或指定协调线程。
每一个请求运行选择器在目标线程的运行循环上排队,然后依照请求收到的顺序处理这些请求。
关于运行选择器程序概要和怎样使用它们的很多其它信息,參加Cocoa运行选择器来源(Cocoa
Perform Selector Sources)。
同步成本和性能
同步帮助确保你的代码正确性,但要牺牲性能。使用同步工具会带来延迟甚至无竞争情况。锁和原子操作通常涉及内存屏障和内核级同步的使用,以确保代码正确的被保护。假设锁有竞争。你的线程会堵塞并经历更大的延迟。
表4-2 列出无竞争情况下相互排斥锁互原子操作的近似成本。
这些測量值代表几千个样本的平均时间。与线程创建时间一样,相互排斥锁获取时间(即便在无竞争情况下)依据处理器负载、计算机速度、可用系统的数量和程序内存的数量有非常大的不同。
表4-2 相互排斥锁和原子操作成本 |
||
项目 |
大概成本 |
说明 |
Mutex acquisition time 相互排斥锁获取时间 |
大约0.2微秒 |
这是在无竞争情况下锁的获取时间。假设锁被两一个线程持有,获取时间会更长。数据由分析基于Intel、2GHz酷睿双核处理器、1GB的RAM执行在OS X v10.5上的iMac上相互排斥锁获取时间生成的平均值和中位值来决定的。 |
Atomic compare-and-swap 原子 比較-交换 |
大约0.05微秒 |
这是在无竞争情况下的比較-交换时间。 数据由分析基于Intel、2GHz酷睿双核处理器、1GB的RAM执行在OS X v10.5上的iMac上相互排斥锁获取时间生成的平均值和中位值来决定的。 |
当设计你的并发任务,正确性始终是最重要的因素,但你还应该考虑性能因素。在多线程中能正确的执行代码,但比在单线程中执行同样的代码慢,这并非一种进步。
假设你在改造现有的单线程应用。你应该採用一组基线作为关键任务性能的測量值。在加入额外线程,你必须为同样的任务採取新的測量值,并比較多线程与单线程情况下的性能。
假设採用你的代码,线程并没有提高性能。你可能要考虑详细的实现或整个的使用线程。
关于性能和工具来收集度量标准的很多其它信息。參见性能概述(Performance
Overview)。关于锁和原子操作的成本的信息,參见线程成本(Thread
Costs)。
线程安全和信号
当到线程应用时,没什么比处理信号问题引起很多其它的恐惧和混乱。信号时一种低级别的BSD处理机制,可用来交付信息到进程或以某种方式操作它。一些程序使用信号来检測某些事件,比如一个子进程的死亡。
系统使用信号来终止失控的进程和交流其它类型的信息。
信号的问题不是它们做什么,而是当应用有多线程时它们的行为。
在单线程应用中,全部的信号处理程序执行在主线程上。在多线程应用中。信号不绑定到特定硬件错误(如非法指令),而是传递到不论什么一个正在执行的线程。
假设同一时候执行多个线程。信号被发送到碰巧被系统选择的那个。换句话说。信号能够交付到你应用中的不论什么线程。
实现应用中的信号处理的第一个规则时避免假定哪个线程在处理信号。如果一个特定的线程希望处理一个给定的信号,当信号到达时你须要制定出通知线程的几种方式。
你不能如果一个信号处理程序来自一个线程将会发送信号到同样的线程。
关于信号和安装信号处理程序的很多其它信息,參见信号(signal )和sigaction 手冊页。
线程安全设计的小提示
同步工具是一个实用的方法是代码线程安全,但不是灵丹妙药。使用太多,锁和其它类型同步基元能够降低应用的线程性能,相比非线程性能。找到安全域性能之间的平衡是一种艺术须要经验。
下面章节提供提示来帮助你为你的应用选择合适同步级别。
全然避免同步
对于你从事的不论什么新项目甚至现有项目,设计代码和数据结构避免同步是最好的解决方式。
虽然锁和其它同步工具非常实用。它们确实影响应用的性能。假设总体设计引起特定资源的剧烈竞争。你的线程会等待更长的时间。
实现并发的最好方法是降低并发任务间的相互作用和相互依赖关系。假设每一个任务执行在自己是有数据集,它不须要锁来保护这些数据。
即使两个任务共享一个公共数据集的情况下。你能够查看设置的分区或者为每一个任务提供自己的副本。当然。复制数据集也有成本。所以在下决定前你必须权衡这些成本与同步成本。
理解同步的限制
同步工具仅仅有当它们一直用于应用中的全部线程时才是有效的。
假设你创建一个相互排斥锁来限制訪问特定资源,你全部的线程必须在试图操作资源前获取同样的相互排斥锁。假设不这样做,就会破坏相互排斥锁提供的保护。是一个程序猿错误。
注意代码正确性的风险
当使用锁和内存屏障时,你应该细致考虑它们在你代码中的位置。
即便是锁放置好能够使你有一种虚假的安全感。以下一系列的样例尝试举例说明这个问题。指出看似无害代码的缺陷。主要的前提是你有一个包括一组不可变对象的可变数组。
如果你想调用数组中第一个对象的方法。
你可能使用以下的代码:
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject; [arrayLock lock];
anObject = [myArray objectAtIndex:0];
[arrayLock unlock]; [anObject doSomething];
由于数组是可变的,数组的锁能够防止其它线程改动数组直到你得到所需的对象。由于你检索的对象本身是不可变的,在调用doSomething
方法时不须要锁。
只是,前面的样例有个问题。假设你释放锁时。在你有机会运行doSomething
方法之前,还有一个线程进入并从数组中删除全部对象,这将发生什么?在没有垃圾回收应用中,代码持有的对象可能被释放,留下anObject
指向一个无效的内存地址。解决问题,你可能决定冲洗安排现有代码并在调用doSomething
之后释放锁,例如以下所看到的:
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject; [arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject doSomething];
[arrayLock unlock];
通过移动doSomething
调用到锁内部,你的代码保证当该方法被调用时该对象仍然有效。
不幸的是,假设doSomething
方法须要非常长时间来运行,这将导致你的代码长时间持有锁,这将出现性能瓶颈。
代码的问题不是临界区缺乏定义,而是不理解实际问题。
真正的问题是内存管理问题,锁仅仅被其它存在的线程触发。由于锁能够被其它线程释放,一个更好的解决方式是在释放锁之前retainanObject
。对象被释放而且不引起一个潜在的性能损失,这个解决方式攻克了这种实际问题。
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject; [arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject retain];
[arrayLock unlock]; [anObject doSomething];
[anObject release];
尽管前面的样例在本质上是非常easy的,它们说明了一个非常重要的点。当谈到正确性,你的思考必须超越显而易见的问题。内存管理和你设计的其它方面也可能受到多个存在的线程的影响,所以你须要考虑前面的这些问题。此外。你应该总是假定当编译器符合安全时有可能做最糟糕的事情。这样的意思和警惕应该帮助你避免潜在的问题并保证代码行为的正确性。
关于怎样使你的项目线程安全的很多其它样例。见线程安全总结( Thread
Safety Summary)。
当心死锁和活动锁
不论什么时间一个线程试图同一时候获得多个锁,有发生死锁的可能性。当有两个不同线程。一个须要锁并尝试获取到另外一个线程持有的锁时。会发生死锁。其结果是每一个线程永久堵塞由于它不能获取其它的锁。
活动锁类似死锁。当两个线程竞争同样的一组资源时发生。在活动锁的情况下,一个线程为了获取第二个锁而放弃它的第一个锁。一旦它获取到第二个锁,它能够返回并尝试再次获取第一个锁。由于它花费了全部时间来释放一个锁并试图获取其它锁而非做不论什么实际工作。所以它会锁起来。
避免死锁和活动锁的情况最好的方法就是一个时间仅仅採用一个锁。
假设你必须在时间内获取多个锁,你应该确保其它线程不尝试类似的事情。
正确使用不稳定变量
假设你已经使用相互排斥锁来保护一段代码,不要自己主动假设你须要使用volatile
keyword来保护重要变量。相互排斥锁包括一个内存屏障来确保适当的载入和存储操作的顺序。在重要部分中加入volatile
keyword到变量将强制该值在每次訪问时从内存载入。两种同步技术的结合可能在特定情况下是必要的,但会导致重大性能损失。
假设相互排斥锁足以保护变量,省略volatile
keyword。
相同重要的是,你不要为了避免使用相互排斥锁而不用不稳定变量。一般来说,相互排斥锁和其它同步机制比不稳定变量更好的保护数据结构的完整性。Volatilekeyword仅仅确保从内存载入一个变量而不是存储在寄存器中。
它不保证代码訪问变量的正确性。
使用原子操作
非堵塞同步是运行某些类型的操作并避免浪费锁的一种方式。尽管锁是同步两个线程的一种有效方式。获取一个锁是一个相对昂贵的操作,即使在无竞争的情况下。
相比之下,很多原子操作仅仅花费一小部分时间来完毕而且与锁一样有效。
原子操作让你在32位或64位值上运行简单的数学和逻辑操作。这些操作依赖于特殊指令(和一个可选的内存屏障)来保证在再次訪问内存前完毕给定的操作。在多线程的情况下,你应该总是使用原子操作。包括内存屏障来保证线程间内存同步正确。
表4-3 列出了可用原子数学和逻辑操作以及相应的函数名称。这些函数都定义在/usr/include/libkern/OSAtomic.h
头文件里,你能够找到完整的语法。
这些函数的64位版本号仅仅能用在64位进程中。
表4-3 原子数学和逻辑操作 |
||
操作 |
函数名 |
描写叙述 |
Add 加法 |
两个数值相加并将结果存储在一个指定变量中。 |
|
Increment 增量 |
指定整数值增量为1 |
|
Decrement 减量 |
指定整数值减量为1 |
|
Logical OR 逻辑或 |
在指定的32位值和32位掩码之间运行逻辑或 |
|
Logical AND 逻辑与 |
在指定的32位值和32位掩码之间运行逻辑与 |
|
Logical XOR 逻辑异或 |
在指定的32位值和32位掩码之间运行逻辑异或 |
|
Compare and swap 比較并交换 |
OSAtomicCompareAndSwap32Barrier OSAtomicCompareAndSwap64Barrier OSAtomicCompareAndSwapPtrBarrier |
将一个变量与指定的旧值比較。 假设这两个值相等。这个函数将指定新值赋给变量,否则,它什么都不做。比較和分配是一个原子操作。函数返回一个布尔值表明交换是否实际发生。 |
Test and set 測试并设置 |
測试指定变量的bit,设置bit为1,并返回旧bit的值作为一个布尔值。Bit依据 比如,測试32bit整数最低顺序bit(bit |
|
Test and clear 測试并清除 |
測试指定变量的bit,设置bit为0,并返回旧bit的值作为一个布尔值。 Bit依据 这个公式有效的将变量分解为8bit大小的数据块并逆序排列每一个数据块中的bit。比如,測试32bit整数最低顺序bit(bit |
大多数原子函数行为应该相对简单并符合你的期望。然而,清单4-1中展示了原子的測试-设置和比較-交换操作的行为,这些更加复杂些。
前三个调用OSAtomicTestAndSet
函数演示bit操作公式怎样运用到一个整数值上,结果可能不同于你的预期。最后两个调用展示了OSAtomicCompareAndSwap32
函数的行为。在全部情况下。这些函数在没有其它线程操作该值的情况下被调用。
清单4-1 运行原子操作
int32_t theValue = 0;
OSAtomicTestAndSet(0, &theValue);
// theValue is now 128. theValue = 0;
OSAtomicTestAndSet(7, &theValue);
// theValue is now 1. theValue = 0;
OSAtomicTestAndSet(15, &theValue)
// theValue is now 256. OSAtomicCompareAndSwap32(256, 512, &theValue);
// theValue is now 512. OSAtomicCompareAndSwap32(256, 1024, &theValue);
// theValue is still 512.
关于原子操作的信息,參见原子(atomic )手冊页和 /usr/include/libkern/OSAtomic.h
头文件。
使用锁
锁是线程编程的一个基本同步工具。
锁让你能够非常easy的保护大部分代码,这样你就能够保证代码的正确性。OS X和iOS为全部运用类型和基础框架提供主要的相互排斥锁。定义了一些额外变量的相互排斥锁的特殊情况。下面章节向你展示怎样使用这些类型的锁。
使用POSIX相互排斥锁
POSIX相互排斥锁在不论什么应用中都很easy使用。
声明和初始化一个pthread_mutex_t
结构来创建相互排斥锁。使用pthread_mutex_lock
函数和pthread_mutex_unlock
函数来锁定和解锁相互排斥锁。清单4-2展示了初始化和使用一个POSIX相互排斥锁的基本代码。
当你完毕锁,仅仅需调用pthread_mutex_destroy
来释放锁的数据结构。
清单4-2 使用相互排斥锁
pthread_mutex_t mutex;
void MyInitFunction()
{
pthread_mutex_init(&mutex, NULL);
} void MyLockingFunction()
{
pthread_mutex_lock(&mutex);
// Do work.
pthread_mutex_unlock(&mutex);
}
注意:前面的代码是为了展示POSIX线程相互排斥锁函数的基本使用方法的一个简化样例。你自己的代码应该检查这些函数返回的错误代码并适当的处理它们。
使用NSLock类
NSLock对象实现Cocoa应用中基本相互排斥锁。
全部锁(包含NSLock)的接口实际上是由 NSLocking协议来定义的,它定义了lock
和
unlock
方法。类似相互排斥锁,你使用这些方法来获取和释放锁。
除了标准锁定行为,NSLock
类加入tryLock 和 lockBeforeDate:方法。tryLock 方法尝试获取锁,假设锁不可用,不堵塞线程;相反。该方法仅仅返回NO。
lockBeforeDate: 方法尝试获取锁,假设在指定的时间限制内没有获取到锁,并不堵塞线程(返回NO)。
以下的样例展示了怎样使用NSLock
对象来协调视觉显示的更新。其数据是多个线程计算而来。假设线程不能马上获取锁,它仅仅是继续计算直到它能够获取锁并更新显示。
BOOL moreToDo = YES;
NSLock *theLock = [[NSLock alloc] init];
...
while (moreToDo) {
/* Do another increment of calculation */
/* until there’s no more to do. */
if ([theLock tryLock]) {
/* Update display used by all threads. */
[theLock unlock];
}
}
使用@synchronized指令
在Objective-C代码上创建相互排斥锁的一个方便的方式是@synchronized
指令。@synchronized
指令做其它相互排斥锁做的事,防止不同的线程同一时候获取同样的锁。
然而。在这样的情况下。你不必直接创建相互排斥锁或锁对象。相反,你仅仅需使用不论什么Objective-C对象作为锁记号,如演示样例所看到的:
- (void)myMethod:(id)anObj
{
@synchronized(anObj)
{
// Everything between the braces is protected by the @synchronized directive.
}
}
传递到@synchronized
指令的对象是一个独特的标识符用来区分protectblock。
假设你在两个不同线程上运行前面的方法,传递不同的对象作为每一个线程上的anObj
參数,每一个线程使用自己的锁并继续处理而不被其它线程堵塞。然而。假设在这两种情况下,你传递同样对象,当中一个线程获取锁,其它的线程将堵塞直到第一个线程完毕关键部分。
作为防范措施,@synchronized
block隐式的将一个异常处理程序加入到受保护的代码。
这个处理程序自己主动释放相互排斥锁时会抛出异常。这意味着未来使用@synchronized
指令。你必须启用Objective-C代码中的异常处理。假设你不希望隐式异常处理程序引起额外的开销,你应该考虑使用锁类。
关于@synchronized
指令的很多其它信息,參见Objective-C编程语言(TheObjective-C Programming Language)。
使用其它Cocoa锁
下面章节描写叙述使用其它类型的Cocoa锁的过程。
使用NSRecursiveLock对象
NSRecursiveLock 类定义了一个锁,同样线程能够多次获取该锁而不引起线程死锁。递归锁记录它被成功获取的次数。每一个成功获取的锁必须通过对应的调用解锁来平衡。
仅仅有当全部的锁定和解锁调用平衡。才干释放锁这样其它线程能够获取它。
顾名思义,这样的类型的锁通经常使用来递归函数中来防止递归堵塞线程。
你相同能够在非递归情况下使用,调用函数语义上要求他们获得锁。
这是一个简单递归函数的样例。在递归过程中获取锁。假设你没有在这段代码中使用NSRecursiveLock
对象,当函数再次被调用,线程将死锁。
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init]; void MyRecursiveFunction(int value)
{
[theLock lock];
if (value != 0)
{
--value;
MyRecursiveFunction(value);
}
[theLock unlock];
} MyRecursiveFunction(5);
注意:由于递归锁没有被释放直到全部锁定调用与解锁调用相平衡,你应该细致权衡使用性能锁与潜在的性能影响。在一段时间内持有不论什么锁会导致其它线程堵塞直到递归完毕。假设你能重写代码消除递归或消除须要使用的递归锁,你能够获得更好的性能。
使用一个NSConditionLock对象
一个NSConditionLock 对象定义了一个相互排斥锁。该锁能够锁定或解锁为特定值。你不应该将这样的类型的锁与条件(见条件( Conditions))混淆。该行为有点类似条件,但实现是全然不同的。
通常。当线程须要以特定顺序运行任务时,你能够使用NSConditionLock
对象,比如,当一个线程产生数据,还有一个线程消耗数据。当生产者在运行的同一时候消费者使用项目中明白的条件获取锁。(条件本身仅仅是一个你自己定义的整数值。)当生产者完毕,它打开锁并设置锁条件为适当的整数值来唤醒消费者线程,然后继续处理数据。
NSConditionLock
对象对应的锁定和解锁方法能够在不论什么组合中使用。比如,你能够把unlockWithCondition:锁定消息与lockWhenCondition: 解锁消息组成一对。当然,后一种组合解锁但不会释放不论什么等待一个特定条件值的线程。
以下的样例展示了怎样使用条件锁处理生产者-消费者问题。
如果一个原因包括一个队列的数据。生产者线程加入数据到队列中,消费者线程从队列中提取数据。生产者不须要等待一个特定的条件。但它必须等待锁可用。安全的将数据加入到队列。
id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA]; while(true)
{
[condLock lock];
/* Add data to the queue. */
[condLock unlockWithCondition:HAS_DATA];
}
由于锁的初始条件设置为NO_DATA
,
生产者线程最初获取锁应该没有问题。
它填补了队列数据并设置条件为HAS_DATA
。
在随后的迭代中,生产者线程在数据到达时加入新数据,不管队列是空或仍有一些数据。
它唯一堵塞的时间是当消费者线程从队列中提取数据。
由于消费者必须有数据处理,它使用特定的条件等待队列。当生产者将数据放置到队列中。消费者线程醒来并获取锁。然后它能够从队列中提取一些数据并更新队列状态。以下的样例展示了消耗者线程处理循环的基本结构。
while (true)
{
[condLock lockWhenCondition:HAS_DATA];
/* Remove data from the queue. */
[condLock unlockWithCondition:(isEmpty ? NO_DATA : HAS_DATA)]; // Process the data locally.
}
使用NSDistributedLock对象
NSDistributedLock
类可用于多主机上的多应用限制訪问某些共享资源,比如一个文件。锁本身实际上是一个相互排斥锁。使用文件系统项目实现。比如一个文件或文件夹。NSDistributedLock
对象可用。锁对全部使用它的应用必须是可写的。这通常意味着将它放置到文件系统,能够訪问全部执行该应用的计算机。
不像其它类型的锁,NSDistributedLock
不符合NSLocking 协议,因此没有lock方法。
Lock方法将堵塞线程的运行,须要系统以特定速率轮询锁。而不是利用代码中的这个坏处,NSDistributedLock
提供一个tryLock
方法让你决定是否轮询。
由于它使用文件系统实现,NSDistributedLock
对象不释放除非全部者显式的释放它。假设你的应用在持有一个分布式锁时崩溃。其它客户将无法訪问受保护的资源。在这样的情况下,你能够使用breakLock
方法来打破现有的锁,这样你能够获取到它。通常应该避免打破锁,除非你确定拥有进程死亡而且不能释放锁。
正如其它类型的锁,当你完毕使用NSDistributedLock
对象,你通过调用unlock方法来释放它。
使用条件
条件是一种特殊类型的锁。你能够使用它来同步必须继续操作的顺序。它与相互排斥锁有微妙的不同。等待条件的线程仍然堵塞直到还有一个线程显式的发送条件信号。
因为实现操作系统所涉及的微妙之处,条件锁同意返回假的成功,即使它们实际上并没有收到代码的信号。为了避免这些虚假信号引起的问题,你应该在条件锁结合处使用断言。断言是一个更详细的方法确定继续的线程是否安全。条件仅仅是保持你的线程休眠直到断言能够被发送信号的线程设置。
以下的章节展示了怎样在代码中使用条件。
使用NSCondition类
NSCondition 类提供与POSIX条件类似的语义,可是封装所需的锁和条件数据结构到一个单一的对象。结果是一个对象,你能够像相互排斥锁一样锁定然后像条件一样等待。
清单4-3展示了代码片段演示事件序列等待NSCondition
对象。cocoaCondition
变量包括一个NSCondition
对象,timeToDoWork
变量是一个整数。在发送条件信号之前马上从还有一个线程添加。
清单4-3 使用Cocoa条件
[cocoaCondition lock];
while (timeToDoWork <= 0)
[cocoaCondition wait]; timeToDoWork--; // Do real work here. [cocoaCondition unlock];
清单4-4展示了用于发送Cocoa条件和断言变量的增量的代码。
你应该在发送信号之前锁定条件。
清单4-4 发送Cocoa条件信号
[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];
使用POSIX条件
POSIX线程条件锁须要使用条件数据结构和相互排斥锁。尽管两个锁结构不同。相互排斥锁在执行时与条件结构密切相关。线程等待一个信号总是使用同样的相互排斥锁和条件结构。改变配对会导致错误。
清单4-5展示了条件和断言的基本初始化和使用。
在初始化条件和相互排斥锁后,等待线程进入一个使用ready_to_go
变量做外断言的while循环。仅仅有当断言设置且随后发送条件信号,等待的线程才会醒来并開始它的工作。
清单4-5 使用POSIX条件
pthread_mutex_t mutex;
pthread_cond_t condition;
Boolean ready_to_go = true; void MyCondInitFunction()
{
pthread_mutex_init(&mutex);
pthread_cond_init(&condition, NULL);
} void MyWaitOnConditionFunction()
{
// Lock the mutex.
pthread_mutex_lock(&mutex); // If the predicate is already set, then the while loop is bypassed;
// otherwise, the thread sleeps until the predicate is set.
while(ready_to_go == false)
{
pthread_cond_wait(&condition, &mutex);
} // Do work. (The mutex should stay locked.) // Reset the predicate and release the mutex.
ready_to_go = false;
pthread_mutex_unlock(&mutex);
}
发送信号线程负责设置断言并发送条件锁信号。清单4-6展示实现该行为的代码。在这个样例中。条件在相互排斥锁内部发送信号。以防止等待条件的线程间发生竞争条件。
清单4-6 发送条件锁信号
void SignalThreadUsingCondition()
{
// At this point, there should be work for the other thread to do.
pthread_mutex_lock(&mutex);
ready_to_go = true; // Signal the other thread to begin work.
pthread_cond_signal(&condition); pthread_mutex_unlock(&mutex);
}
注意:前面的代码是一个简化的样例来展示POSIX线程条件函数的基本使用方法。
你自己的代码应该返回错误代码。
官方原文地址:
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html#//apple_ref/doc/uid/10000057i-CH8-SW1http://www.apple.com/legal/policies/ideas.html
-//apple_ref/doc/uid/_blank