在应用程序中的多个线程的存在开辟了潜在的问题,关于安全访问到资源从多个执行线程。两个线程修改相同的资源可能会相互干扰,以意想不到的方式。例如,一个线程可能会覆盖其他人的更改或应用程序置于未知和潜在无效的状态。如果你很幸运,已损坏的资源可能会导致明显的性能问题或相对容易,跟踪并修复的崩溃。如果你运气不好,然而,腐败可能导致不直到很久以后,表现自己的细微错误或错误可能需要你基本的编码假设任何重大改革。
线程安全的时候,一个好的设计是最好的保护你。避免共享的资源,减少您的线程之间的交互就不太可能,这些线程,互相干扰。完全无干涉的设计并不总是可能的然而。在哪里您的线程必须进行交互的情况下,您需要使用同步工具来确保当它们交互,他们这样做安全。
OS X 和 iOS 提供许多的同步工具,帮助您使用,从提供给那些在您的应用程序中正确的事件序列的相互独占访问的工具。以下各节描述了这些工具以及您如何使用它们在您的代码,会影响到你的程序的资源的安全访问。
若要防止意外更改的数据从不同的线程,你可以任意设计您的应用程序,没有同步问题或您可以使用同步工具。虽然一共避免同步问题是可取的但它并不总是可能。以下各节描述了同步工具可供您使用的基本范畴。
原子操作是同步的简单形式的工作在简单数据类型。原子操作的优点是不会阻塞争用线程。对于简单的操作如递增计数器变量,这可以导致更好的表现比采用锁。
OS X 和 iOS 包括许多行动来执行基本的数学和逻辑运算,在 32 位和 64 位的值。这些操作中,原子版本的比较和交换、 测试集和测试和清除操作。支持的原子操作的列表,请参阅/usr/include/libkern/OSAtomic.h
头文件或参阅atomic
的手册页。
为了实现最佳性能,编译器通常重新排列程序集级指令保持尽可能详尽地处理器指令管线。作为这种优化的一部分,编译器可能在它认为这样做不能生成不正确的数据时访问主存储器的指令重新排序。不幸的是,并非总是可能为编译器检测内存相关的所有操作。如果看似独立的变量实际上互相影响,编译器优化可以更新这些变量以错误的顺序生成潜在错误的结果。
一个内存屏障是一种非阻塞同步类型工具,用来确保内存按正确的顺序发生的操作。一个内存屏障就像一个屏障,迫使处理器来完成任何负载和存储位于屏障,它允许执行加载和存储操作后屏障定位之前的操作。内存屏障通常被用来确保内存操作由一个线程
(但可见到另一个) 始终预期的顺序出现。缺乏一个内存屏障,在这种情况可能允许其他线程看到看似不可能的结果。(例如,见内存屏障的维基百科词条)。聘请一个内存屏障,你只是调用OSMemoryBarrier
函数在适当时候在您的代码中。
Volatile 变量适用于单个变量的另一种类型的内存约束。编译器经常通过加载到寄存器的变量的值来优化代码。用于本地变量,通常这并非一个问题。如果变量不过是从另一个线程可见,这样的优化可能会阻止其他线程注意到对它的任何更改。volatile
关键字应用到一个变量强制编译器,它用每次从内存加载该变量。你可能会为volatile
声明一个变量,如果它的值可以在任何时间改变由编译器可能不能够检测到外部源。
因为内存屏障和 volatile 变量减少的优化编译器可以执行的次数,他们应该谨慎使用,只在需要确保正确性。有关使用内存屏障的信息,请参阅OSMemoryBarrier
手册页。
锁是最常用的同步工具之一。你可以使用锁来保护您的代码,这是一段代码每次只有一个线程允许访问临界区。例如,一个临界区可能操纵一种特定的数据结构或使用的一些资源,支持最多的一位客户一次。通过在该节周围放置一把锁,您排除其他线程进行更改可能会影响您的代码的正确性。
表 4-1列出了一些常用的程序员的锁。OS X 和 iOS 提供大部分的这些锁的类型,但并不是所有的实现。对于不支持的锁定类型,说明列解释了为什么那些锁不直接在平台实现的原因。
锁 |
描述 |
---|---|
互斥锁 |
A 相互独占 (或互斥体) 锁行为作为周边资源的防护屏障。互斥体是信号的一种的一次只有一个线程访问。如果互斥体是在使用另一个线程试图获取它,该线程块,直到互斥锁被释放由其原持有人。如果多个线程争夺相同的互斥体,只是一次一个允许访问到它。 |
递归锁 |
递归锁是一个变种对互斥锁。递归锁允许一个线程获取锁多次发布之前。其他线程保持被阻滞状态,直到锁的所有者释放该锁它获取了它的相同次数。递归锁主要使用递归迭代过程中,但也可能在多个方法需要单独获取锁的情况下使用。 |
读-写锁 |
读-写锁也被称为共享独占锁。这种类型的锁通常用在更大规模的行动,如果受保护的数据结构是频繁地读取和修改只是偶尔可以显著提高性能。在正常操作过程中多个读取器可以同时访问的数据结构。当一个线程想要写入该结构时,它虽然,阻塞,直到所有的读者都释放的锁,而此时它的锁和可更新的结构。虽然写线程正在等待锁,新阅读器线程阻塞,直到写入线程完成。该系统支持使用
POSIX 线程只能读写锁。有关如何使用这些锁的详细信息,请参阅 |
分布式的锁 |
分布式的锁提供互斥访问在流程级别。不像真正的互斥锁,分布式的锁不阻止进程或防止它运行。它只报告当锁正忙着,让决定如何继续的过程。 |
自旋锁 |
一个旋转锁轮询其锁条件反复直到该条件变为 true。自旋锁是最常用在多处理器系统上一把锁的预期的等待时间在哪里小。在这些情况下,它通常是更有效地轮询比若要阻止的线程,包括上下文切换和更新线程的数据结构。系统未提供任何实现自旋锁由于其轮询的性质,但您可以轻松地实现他们的具体情况。在内核中实现自旋锁的信息,请参阅内核编程指南. |
双重检查的锁定 |
双重检查的锁定是尝试减少以测试锁定标准采取锁锁的开销。双重检查的锁定是潜在的不安全的因为系统未提供明确的支持,对他们来说并不鼓励他们使用。 |
有关如何使用锁的信息,请参阅使用锁.
一个条件是信号的另一种类型的允许线程彼此的信号,当某个条件为真时。条件通常用于指示资源的可用性,或以确保按特定的顺序执行任务。当一个线程测试一个条件时,它可以阻止除非这种情况已经是真实。它仍然是阻止,直到某个其他线程显式更改和信号的条件。一个条件和互斥锁之间的区别是多个线程可能会在同一时间允许访问的条件。条件是多一个让不同的线程,通过一些指定的条件取决于门的看门人。
您可能使用条件的一种方法是管理一个池的挂起事件。事件队列将使用条件变量到信号正在等待的线程,当有事件队列中。如果一个事件发生时,该队列将正确传递信号的条件。如果一个线程已经等待时,它会被吵醒于是将拉从队列的事件,并对其进行处理。如果两个事件在大致相同的时间进来到队列,队列将信号条件下两次,两个线程中醒来。
该系统为条件在几个不同的技术提供支持。正确实施条件需要细致的编码,然而,所以你应该看在您自己的代码中使用它们之前的示例中使用的条件。
可可粉的应用程序都在向单个线程同步的方式传递信息的简便方法。NSObject
类声明在一个应用程序的活动线程上执行一个选择器的方法。这些方法允许您传送消息以异步方式与保证他们将同步由目标线程的线程。例如,您可以使用选择器消息交付结果从一个分布式的计算,到您的应用程序的主线程或到指定的协调员线程执行。排队目标线程运行环路上的每个请求执行一个选择器和请求然后按顺序处理收到的顺序。
为总结执行选择器例程和如何使用它们,请参阅可可执行选择器来源有关的详细信息.
同步可帮助确保您的代码的正确性,但这样做会降低性能。利用同步工具,介绍了拖延,甚至在无异议的情况下。锁定和原子操作通常涉及使用内存屏障和内核级同步,以确保代码适当的保障。如果有一把锁的争用,您的线程可以阻止和经验甚至更多延误。
表 4-2列出了一些与互斥体和在无异议的情况下的原子操作相关联的近似成本。这些测量代表接管几个一千个样品的平均时间。随着与线程创作时代虽然互斥体采集时间 (即使在无争议的情况下) 可以极大地取决于处理器加载,电脑和的系统和程序的可用内存量的速度。
项目 |
近似成本 |
备注 |
---|---|---|
互斥体捕获时间 |
大约 0.2 微秒 |
这是在无异议的情况下锁定采集时间。如果由另一个线程持有锁,捕获时间可以更大。数字测定分析的均值和中位数在基于 Intel 的 imac 2 g h z 酷睿处理器和 1GB 的 RAM 运行 OS X v10.5 获取互斥锁的过程中生成的值。 |
原子的比较和交换 |
大约 0.05 微秒 |
这是无可争议的案件中比较和交换次。数字分析的均值和中位数用于操作的值,确定了,并在基于英特尔的酷睿 2 GHz 处理器和 1GB 的 RAM 运行 OS X v10.5 iMac 上生成。 |
当设计你的并发任务,正确性是最重要的因素,但是您还应该考虑性能因素。正确执行下多个线程,但低于相同的代码在单独的线程上运行的代码是很难改善。
如果你改造现有的单线程应用程序,你应该总是一套基线测量性能的关键任务。后添加额外的线程,然后应该采取新的测量方法,为这些相同的任务,并比较性能的多线程的单线程的情况。如果后调优您的代码,线程不能提高性能,要完全重新考虑您的特定实现或线程的使用。
有关性能和工具来收集度量标准的信息,请参阅性能概述。有关成本的锁定和原子操作的特定信息,请参阅线程的成本.
线程应用程序的时候,什么都不会导致更多恐惧或混乱而不会这一问题的处理信号。信号是一种低级的 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
方法需要很长的时间来执行,这可能会导致您代码持有锁的很长的时间,可以创建一个性能瓶颈。
代码的问题不是关键区域定义不清,但实际问题并不理解。真正的问题是只能由其他线程存在触发的内存管理问题。因为它可以由另一个线程释放,更好的解决方案将释放该锁之前保留anObject
。此解决方案涉及的实际问题,正在释放对象,这样做没有介绍潜在的性能缺陷。
NSLock* arrayLock = GetArrayLock(); |
NSMutableArray* myArray = GetSharedArray(); |
id anObject; |
|
[arrayLock lock]; |
anObject = [myArray objectAtIndex:0]; |
[anObject retain]; |
[arrayLock unlock]; |
|
[anObject doSomething]; |
[anObject release]; |
虽然前面的示例非常简单,自然,他们说明了很重要的一点。正确性的时候,你得想超越明显的问题。内存管理和设计的其他方面可能也受存在多个线程,所以你要想想前面这些问题。此外,您应该总是假定编译器会做最坏的可能事,安全的时候。这种意识和警惕性应该帮助你避免潜在的问题,并确保您的代码的行为正确。
如何使您的程序线程安全的更多示例,请参见线程安全摘要.
任何线程尝试在同一时间,采取多个锁定的时间有可能会发生死锁。当两个不同的线程持有的锁,另一个需要,然后尝试获取由另一个线程持有的锁的时候,就会发生死锁。结果是了,每个线程块永久因为它可以永远不会获得其他的锁。
锁是类似于一个死锁,当两个线程争夺相同的资源集时发生。在锁情况下,一个线程放弃其第一个锁在企图收购它的第二个锁。一旦它的第二次的锁,它可追溯,并尝试再次获取第一个锁。它锁定,因为它花费所有时间释放一个锁和尝试获取其他的锁,而不是做任何实际的工作。
最好的方法,以避免死锁和活锁的情况是,一次只能有一个锁。如果您必须一次获得多个锁,您应该确保其他线程不要试图做同样的事情。
如果你已经在使用一个互斥锁保护的代码段,不要自动假定你需要使用volatile
关键字来保护该节内的重要变量。互斥体包括一个内存屏障,以确保合适的次序的负载和存储操作。volatile
关键字添加一个变量在一个临界区部队要每一次访问它时从内存加载的值。两个同步技术的结合可能需要在特定情况下,但也会导致重大的性能损失。如果互斥对象仅足以保护该变量,请省略volatile
关键字。
它也是重要的你做不使用 volatile 变量在试图避免使用互斥锁。一般情况下,互斥锁和其他同步机制是更好的方式来保护你比 volatile 变量的数据结构的完整性。volatile
关键字只确保变量是从内存加载,而不是存储在寄存器中。它不能确保您的代码正确访问的变量。
非阻塞同步是一个办法执行某些类型的操作,避免锁的开支。虽然锁是有效的方法来同步两个线程,获取锁是相对昂贵的操作,即使在无异议的情况下。相比之下,许多原子操作需要的时间即可完成一小部分就像一把锁一样有效。
原子操作使您可以执行简单的数学和逻辑运算,在 32 位或 64 位的值。这些操作依赖于特殊的硬件指令 (和可选内存屏障),以确保在给定的操作完成之前再次访问受影响的内存。在多线程的情况下,您应始终使用包含一个内存屏障,以确保内存正确同步线程之间的原子操作。
表 4-3列出了可用的原子数学和逻辑运算和相应的函数名称。所有在/usr/include/libkern/OSAtomic.h
头文件中,在那里你还可以找到完整的语法来声明这些函数。这些函数的
64 位版本是仅在 64 位进程中可用。
操作 |
函数名称 |
描述 |
---|---|---|
添加 |
将两个整数值相加,并将结果存储在指定的变量之一。 |
|
增量 |
指定的整数值递增 1。 |
|
减量 |
递减指定的整数值 1。 |
|
逻辑或 |
执行逻辑或 32 位掩码与指定的 32 位值。 |
|
逻辑和 |
执行逻辑与 32 位掩码与指定的 32 位值。 |
|
逻辑 xor 运算 |
执行逻辑异或指定的 32 位值和一个 32 位掩码。 |
|
比较与交换 |
|
比较针对指定的旧值的变量。如果这两个值相等,此函数将指定的新值分配给变量 ;否则,它没有。作为一个原子操作做了比较和赋值和函数返回布尔值,该值指示是否实际发生了互换。 |
测试和设置 |
测试有点在指定的变量中,将此位设置为 1,并返回旧位作为布尔值的值。Bits 根据公式来测试 |
|
测试和清除 |
测试有点在指定的变量中,将此位设置为 0,并返回一个布尔值,作为老位的值。Bits 根据公式来测试 |
大多数原子函数的行为应该是相对比较简单,你会期望。然而,清单 4-1,表明原子的测试集和比较和交换操作,稍微复杂的行为。第三个调用OSAtomicTestAndSet
函数表明位操纵公式正在使用一个整数值,而其结果可能从你期望有何不同。最后两个来电显示OSAtomicCompareAndSwap32
函数的行为。在所有情况下,这些函数的调用在无异议的情况下当没有其他线程正在操作的值。
执行原子操作
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
头文件。
锁是线程编程基本同步工具。锁使您能够轻松地保护大段代码,以便您可以确保该代码的正确性。OS X 和 iOS 为所有应用程序类型提供了基本的互斥锁和基础架构定义一些互斥锁的特殊情况下的其他款式。以下各节说明了如何使用这些锁类型的几个。
POSIX 互斥锁是非常容易使用,从任何应用程序。若要创建互斥锁,您声明并初始化一个pthread_mutex_t
结构。若要锁定和解锁互斥锁,则使用pthread_mutex_lock
和pthread_mutex_unlock
功能。清单
4-2显示初始化和使用 POSIX 线程互斥锁所需的基本代码。当你完成这个锁定时,只需调用pthread_mutex_destroy
以释放锁数据结构。
pthread_mutex_t mutex; |
void MyInitFunction() |
{ |
pthread_mutex_init(&mutex, NULL); |
} |
|
void MyLockingFunction() |
{ |
pthread_mutex_lock(&mutex); |
// Do work. |
pthread_mutex_unlock(&mutex); |
} |
NSLock
对象实现可可应用程序基本的互斥锁。所有的锁
(包括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
指令是很方便地在目标 C 代码中动态创建互斥锁。@synchronized
指令做出任何其他互斥锁会做
— — 它可以防止不同的线程在同一时间获得相同的锁。在这种情况下,你却不需要直接创建该互斥体或锁定的对象。相反,您只需使用任何目标 C 对象,作为一个锁定的标记,如下面的示例所示:
- (void)myMethod:(id)anObj |
{ |
@synchronized(anObj) |
{ |
// Everything between the braces is protected by the @synchronized directive. |
} |
} |
对象传递给@synchronized
指令是用来区分受保护的块的唯一标识符。如果你在两个不同的线程中执行前面的方法,传递anObj
参数不同的对象在每个线程,每个会采取它的锁并没有被阻止由其他继续处理。如果你通过相同的对象在这两种情况下,然而,其中一个线程会第一次获取锁,另将被阻塞,直到第一个线程完成的关键部分。
作为预防措施, @synchronized
块隐式地将异常处理程序添加到受保护的代码。此处理程序会自动释放互斥锁事件中引发的异常。这意味着要使用@synchronized
指令,您还必须启用目标
C 中异常处理代码。如果你不想引起隐式异常处理程序的额外开销,您应该考虑使用的锁类。
关于@synchronized
指令的详细信息,请参阅目标 C 编程语言.
以下各节描述了使用几种其他类型的可可锁的过程。
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
对象,当线程需要按特定的顺序,例如当一个线程产生另一种消耗的数据执行任务。而执行的生产者,消费者获取锁使用特定于您的程序的一个条件。(条件本身是只是一个整数值,您定义)。当制作完成后时,它开启的锁和锁条件设置为相应的整数值,以唤醒使用者线程,然后继续处理数据。
NSConditionLock
对象响应的锁定和解锁的方法可以用于任意组合。例如,您可以将配对与lock
消息unlockWithCondition:
,或lockWhenCondition:
unlock
的消息。当然,这后者的组合解锁锁定,但可能不释放任何线程等待一个特定的条件值。
下面的示例演示如何可能出现的生产者-消费者问题处理使用条件锁定。想象一下,一个应用程序包含队列的数据。一个生产者线程将数据添加到队列中,和使用者线程从队列中提取数据。生产者不需要等待一个特定的条件,但它必须等待锁可用,因此它可以安全地将数据添加到队列。
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
并不符合NSLocking
协议,因而没有lock
的方法。lock
方法会阻止该线程的执行,并要求系统轮询锁在预定的速度。而比这刑罚对您的代码时, NSDistributedLock
提供tryLock
方法,并允许您决定是否不是轮询一次。
因为它使用文件系统实现,除非所有者显式地释放它不释放一个NSDistributedLock
对象。如果您的应用程序崩溃时持有一个分布式的锁,其他客户端将无法访问受保护的资源。在这种情况,您可以使用breakLock
方法,以打破现有的锁定,以便您可以获得它。破碎的锁应一般避免,不过,除非您确信所拥有的进程死并不能释放该锁。
与其他类型的锁,当你完成你使用NSDistributedLock
对象,通过调用unlock
的方法释放它。
条件是锁的一种特殊类型,您可以使用同步操作必须出发的顺序。他们在一个微妙的方式不同的互斥锁。等待某一条件的线程仍处于阻塞状态,直到该条件明确由另一个线程终止状态。
由于参与执行操作系统的最微妙之处,条件锁获准返回杂散的成功,即使他们实际上不终止由您的代码。为了避免引起这些杂散信号的问题,你应该总是使用谓词会同与您的条件锁定。谓词是更具体的方式确定是否它是安全的您的线程继续。条件只是保持您的线程睡着直到谓词可以通过信号的线程设置。
以下各节说明了如何在您的代码中使用条件。
NSCondition
类提供
POSIX 条件相同的语义,但是在单个对象包装所需的锁和条件的数据结构。结果是一个对象,你可以像一个互斥锁,然后等像一个条件。
清单 4-3显示了代码片段演示用于在NSCondition
对象上等待的事件序列。cocoaCondition
变量包含对象NSCondition
和timeToDoWork
变量是从前夕信号条件下的另一个线程递增的整数。
使用可可条件
[cocoaCondition lock]; |
while (timeToDoWork <= 0) |
[cocoaCondition wait]; |
|
timeToDoWork--; |
|
// Do real work here. |
|
[cocoaCondition unlock]; |
清单 4-4显示了用于信号可可条件和递增谓词变量的代码。你总是应该向其发出信号之前锁定条件。
信号可可条件
[cocoaCondition lock]; |
timeToDoWork++; |
[cocoaCondition signal]; |
[cocoaCondition unlock]; |
POSIX 线程状态锁定要求使用一个条件的数据结构和一个互斥锁。虽然两个锁结构是独立的互斥锁是紧密地捆绑在运行时条件结构。线程等待信号应始终使用相同的互斥锁和条件结构在一起。更改的配对会导致错误。
清单 4-5显示了基本的初始化和使用条件和谓词。初始化条件和互斥锁,等待线程进入后一种
while 循环使用ready_to_go
变量作为其谓词。只有当设置谓词和随后终止的条件并等待线程醒来,开始做其工作。
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显示了用于实现此类行为的代码。在此示例中,条件被示意在互斥锁防止等待状态的线程之间发生争用条件。
信号条件下锁
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 线程状态函数的基本用法。您自己的代码应该检查这些函数返回的错误代码,并适当地处理它们。
本附录描述了一些关键的框架在 OS X 和 iOS 的高级别线程安全。本附录中的资料会有所变动。
从多个线程使用可可指南包括以下内容:
不可变的对象是线程安全的一般。一旦您创建它们,您可以安全地传递这些对象和从线程。另一方面,可变对象一般不是线程安全。若要使用线程的应用程序中的可变对象,应用程序必须适当地同步。更多的信息,请参阅可变与不可变.
许多对象当作"线程不安全"只是不能从多个线程使用。许多这些对象可以使用从任何线程,只要它是一次只有一个线程。具体局限于应用程序的主线程的对象是这样叫出。
如果你想要使用一个线程来绘制到视图,支架之间的所有绘图代码lockFocusIfCanDraw
和unlockFocus
方法NSView
.
若要使用 POSIX 线程可可,一定要先把可可,进入多线程模式。更多的信息,请参阅使用 POSIX 线程在可可粉中的应用.
有一种误解是线程安全的基础架构和应用程序套件框架不是。不幸的是,这是一个总的概括和一定的误导作用。每个框架都是线程安全的领域也不是线程安全的地区。以下各节描述基础架构的一般线程的安全。
下面的类和函数通常被认为是线程安全的。您可以使用相同的实例,从多个线程而无需首先获取一个锁。
NSCalendarDate
NSConnection
NSDecimal
函数
NSDeserializer
NSDistantObject
NSDistributedLock
NSDistributedNotificationCenter
NSFileManager
(OS
X v10.5 和以后)
NSHost
NSPortCoder
NSPortMessage
NSPortNameServer
NSProtocolChecker
对象分配并保留计数功能
区与记忆功能
下面的类和函数通常不是线程安全。在大多数情况下,您可以使用这些类从任何线程,只要从只有一个线程同时使用他们。检查类文档中的其他详细信息。
NSArchiver
NSHashTable
函数
NSJavaSetup
函数
NSMapTable
函数
NSSerializer
NSTask
NSUnarchiver
用户名称和主目录函数
注意NSSerializer
、 NSArchiver
、 NSCoder
、 NSEnumerator
的对象是自己的线程安全的虽然他们列出在这里因为它不是安全地更改包裹由他们,而他们使用的数据对象。例如,在存档的情况下是不安全来更改正在存档的对象图。一个枚举数为,它是不安全的任何线程会更改枚举的集合。
下面的类必须只从一个应用程序的主线程使用。
NSAppleScript
不可变对象都是一般的线程安全的 ;一旦您创建它们,您可以安全地传递这些对象和从线程。当然,当使用不可变的对象,你仍然需要记住正确使用引用计数。如果你不恰当地释放你没有保留的对象,你可能导致异常以后。
可变对象一般不是线程安全的。若要使用线程的应用程序中的可变对象,应用程序必须同步对他们访问使用锁。(有关详细信息,请参阅原子操作)。一般情况下,集合类
(例如, NSMutableArray
, NSMutableDictionary
)
不是线程安全而言突变的时候。那就是,如果一个或多个线程正在改变相同的数组,可以出现问题。你必须锁定读取和写入操作发生以保证线程安全的景点周围。
即使方法要求返回一个不可变的对象,您应该永远不会简单地假定返回的对象是不可变的。根据此方法的实现,返回的对象可能是可变或不可变的。例如,与NSString
的返回类型的方法实际上可能会返回NSMutableString
由于其实施。如果你想要保证你的对象是不可变的你要变的副本。
可重入性是唯一可能在那里行动"呼唤"对其他在同一对象或不同对象上的操作。保留和释放对象是一种"呼唤"有时会被忽视。
下表列出了可显式重入的部分基础架构。所有其他类可能或可能不是可重入的他们在将来可能会取得折返。从来没有过一个完整的分析,对可重入性,此列表可能不是详尽无遗。
分布式的对象
NSDistributedLock
目标 C 运行时系统会发送initialize
对每类对象类接收任何其他消息之前的消息。这使类有机会设置及其运行时环境它使用之前。在一个多线程的应用程序,运行时保证只有一个线程
— — 碰巧将第一条消息发送到类的线程 — — 执行initialize
方法。如果第二个线程试图将消息发送到类中,仍然在initialize
方法的第一个线程时,第二个线程会阻止,直到该initialize
方法完成执行。与此同时,第一个线程可以继续在类上调用其他方法。initialize
方法不应依赖另一个线程调用方法的类
;如果是这样,两个线程变得僵持不下。
由于 OS X 版本中的 bug 10.1.x 和早些时候,一个线程可以将消息发送到一个类之前执行这类的initialize
方法的另一个线程完成。然后,线程可以访问尚未完全初始化,可能损坏应用程序的值。如果您遇到此问题,您需要要么引入锁,以防止对之前的值的访问之后他们被初始化或强制类初始化其自身成为之前多线程。
每个线程维护它自己的NSAutoreleasePool
对象的堆栈。可可预计可能会
autorelease 池的当前线程堆栈上始终可用。如果池不可用、 对象做不得到释放和你泄漏内存。NSAutoreleasePool
对象自动创建和销毁在基于应用程序套件中,应用程序的主线程,但是第二个线程
(和基础的应用程序) 必须创建之前使用他们自己的可可。如果您的线程半衰期长,可能会生成大量的 autoreleased 对象,您应该定期销毁并创建 autorelease 池 (如应用程序套件并在主线程上)
;否则为 autoreleased 对象积累,您的内存需求量的增长。如果你分离的线程不使用可可,您不需要创建一个 autorelease 池。
每个线程都有一个且只有一个运行的环。每次运行循环,因此每个线程,然而,有其自己的输入模式,确定哪些输入的来源都听了运行的回路运行时集。输入的模式之一运行的循环做不会影响在另一个运行循环中定义的输入的模式,即使他们可能具有相同的名称定义。
如果您的应用程序基于应用套件,但第二个线程 (和基础的应用程序) 必须运行运行的循环本身,主线程运行的回路是自动运行。如果使用分离的线程未进入运行的循环,该线程退出尽快在分离的方法完成后执行。
尽管一些外表, NSRunLoop
类不是线程安全。你应该只从拥有它的线程调用此类的实例方法。
下面的类和函数通常不是线程安全。在大多数情况下,您可以使用这些类从任何线程,只要从只有一个线程同时使用他们。检查类文档中的其他详细信息。
NSGraphicsContext
.有关详细信息,请参见NSGraphicsContext
限制.
NSImage
.有关详细信息,请参见NSImage
限制.
NSResponder
NSWindow
和它的所有子。有关更多信息,请参见窗口限制.
下面的类必须只从一个应用程序的主线程使用。
NSCell
和它的所有子
NSView
和它的所有子。有关详细信息,请参见NSView
限制.
在辅助线程上,您可以创建一个窗口。应用程序套件可确保与窗口关联的数据结构被释放在主线程以避免争用条件。还有一些窗口对象可能会在应用程序中同时处理大量的 windows 泄漏的可能性。
在辅助线程上,您可以创建一个模态窗口。应用程序套件阻止调用辅助线程,主线程运行模式循环时。
应用程序的主线程负责处理事件。主线程是在run
方法阻止NSApplication
通常调用应用程序的main
功能。虽然应用套件继续工作,如果其他线程都参与的事件路径,操作可以发生顺序不正确。例如,如果对关键事件的响应两个不同的线程,钥匙可以收到坏了。通过让主线程处理的事件,你实现一个更加一致的用户体验。一旦收到,事件可以派往第二个线程进行进一步处理,如果需要。
您可以调用postEvent:atStart:
NSApplication
从辅助线程张贴到主线程事件队列的事件的方法。然而对于用户输入事件,不保证顺序。应用程序的主线程是仍然负责处理事件队列中的事件。
应用程序套件是线程安全的一般用其图形函数和类,包括绘制时NSBezierPath
和NSString
类。以下各节介绍了使用特定类的详细信息。关于绘图和线程的其他信息是可用的可可绘图指南.
的NSView
班一般不是线程安全的。应创建、
销毁、 调整大小、 移动和只能从一个应用程序的主线程执行NSView
对象上的其他操作。从第二个线程的绘图是线程安全的只要你加括号调用的绘图调用lockFocusIfCanDraw
和unlockFocus
.
如果应用程序的辅助线程想要导致视图的主线程上绘部分,它必须不那么像display
方法, setNeedsDisplay:
, setNeedsDisplayInRect:
或setViewsNeedDisplay:
.相反,它应该向主线程发送消息或调用这些方法使用performSelectorOnMainThread:withObject:waitUntilDone:
方法相反。
查看系统图形状态 (gstates) 都是每个线程。使用图形国家用于是一个方法,一个单线程的应用程序实现更好的图形性能,但这已经不再是真实。不正确地使用图形国家实际上可以导致绘制效率较低,比绘图在主线程中的代码。
的NSGraphicsContext
班表示由底层图形系统提供的绘图上下文。每个NSGraphicsContext
实例拥有其自己的独立图形状态:
坐标系统、 裁剪、 当前字体,等等。类的一个实例是自动创建的主线程上为每个NSWindow
实例。如果你做任何绘图从辅助线程,专门为该线程创建的NSGraphicsContext
的一个新实例。
如果你做任何绘图从辅助线程,您必须手动刷新您的绘图调用。可可并不自动更新的观点与内容来自第二个线程,所以您需要调用flushGraphics
NSGraphicsContext
当您完成您的绘图的方法。如果您的应用程序从主线程只绘制内容,你不需要刷新您的绘图调用。
一个线程可以创建NSImage
对象、
绘制到图像的缓冲区,并传递到绘图的主线程。在所有线程之间共享基础的图像缓存。图像和缓存的工作原理的更多信息,请参见可可绘图指南.
核心数据框架通常支持线程,虽然有一些应用的使用注意事项。有关这些警告的信息,请参阅核心数据编程指南中的核心数据并发.
核心基础是足够的线程安全如果您小心设计程序,你不应运行任何问题有关的争用线程。它是线程安全的一般的情况下,例如当查询、 保留、 释放和传递不可变对象周围。甚至中央可能会从多个线程中查询的共享的对象是线程安全的可靠。
像可可,核心基础不是线程安全对象或其内容的突变的时候。例如,修改可变数据或可变的数组对象不是线程安全的你可能期望,但既不修改在一个永恒不变的数组对象。为此原因之一是性能,在这种情况下是至关重要的。此外,它通常是不可能达到这一级别的绝对的线程安全。你不能排除,例如,不确定的行为造成的保留对象从集合中获得。在调用保留所包含的对象之前,可能会释放集合本身。
在这些核心基础对象都是要从多个线程访问和突变的情况下,您的代码应该保护免受同时访问在接入点中使用锁来。例如,枚举的一个核心基础数组对象的代码应该使用适当的锁定调用枚举块周围以防别人变异的数组。
特定样式的程序,向用户显示一个图形化的界面。
用来同步对资源的访问的构造。等待某一条件的线程不允许继续直到另一个线程的显式信号的条件。
必须一次只能由一个线程执行的代码的一部分。
A 的一个线程的异步事件的源。输入的源可以是基于端口或手动触发和必须附加到线程的运行循环。
线程终止后未立即收回其资源。可接合线程必须显式分离或加入由另一个线程,可以回收资源之前。可接合线程提供给与他们一起的线程的返回值.
一种特殊类型的线程创建时创建其自己的进程。当一个程序的主线程退出时,这个过程就结束了。
A 锁。可以举行一次只能由一个线程互斥锁。试图获取互斥体,由不同的线程持有提出当前线程用来睡觉直到最后获取锁。
NSOperation
类的一个实例。操作对象换行的代码和数据与任务关联到可执行文件的分部
NSOperationQueue
类的一个实例。运行队列管理的操作对象执行。
应用程序或程序的运行时实例。进程拥有自己的虚拟内存空间和系统资源 (包括港口权利) 独立于那些分配给其他程序。总是包含至少一个线程 (主线程),可以包含任意数量的附加线程的进程。
的代码和可以运行以执行某些任务的资源的组合。程序不需要图形用户界面,虽然图形化应用程序也被视为程序。
可以由同一个线程锁定多次锁。
事件处理循环中,在此期间接收和发送到适当的处理程序事件。
A 的输入的源、 计时器来源和运行的循环观察员与特定名称关联的集合。在特定的"模式"运行时,运行的循环监视只的来源和观察员与该模式相关联。
NSRunLoop
类或CFRunLoopRef
的实例不透明类型。这些对象提供的接口在一个线程中执行事件处理循环。
在一个运行的循环执行的不同阶段的通知的收件人。
一个受保护的变量,限制对共享资源的访问。互斥锁和条件是两个不同类型的信号量。
的工作要执行一个数量。
A 流的执行过程中。每个线程都有其自己的堆栈空间,但否则与同一进程中的其他线程共享内存。
A 的一个线程同步事件的源。计时器在预定的未来时间生成一次性或重复的事件。
此表描述了对线程编程指南的更改.
日期 | 备注 |
---|---|
2014-07-15 | 从列表中的线程安全类的已删除的 NSXMLDocument。 |
2013 年 10 月 22 日 | 添加的 NSXMLParser 和 NSXMLDocument 对线程安全的对象列表。 |
2013-08-08 | 删除过时的信息。 |
2010-04-28 | 更正拼写错误。 |
2009-05-22 | 搬到了并发编程指南的有关操作对象的信息。只是在线程上重点这本书。 |
2008-10-15 | 更新了有关操作对象和操作队列的示例代码。 |
2008-03-21 | 为 iOS 更新。 |
2008-02-08 | 执行一个主要重写和更新线程相关的概念和任务。 |
添加有关配置线程的详细信息。 |
|
成章和添加了有关原子操作、 记忆障碍和 volatile 变量信息重组同步工具部分。 |
|
添加了有关使用和配置运行循环的更多细节。 |
|
从多线程编程的主题的更改的文档标题. |
|
2007-10-31 | 添加了有关的 NSOperation 和 NSOperationQueue 对象的信息。 |
2006-04-04 | 加入一些新的指导方针和更新运行循环有关的信息。验证分布式的对象代码示例的准确性和更新的代码示例在其他若干条款。 |
2005-03-03 | 更新端口例子使用 NSPort 而不是 NSMessagePort。 |
2005 年 01 月 11 日 | 整顿了文章和展开的文档覆盖不仅仅是可可线程技术。 |
线程的概念信息和添加了涵盖不同的线程包在 OS X 中的信息进行更新。 |
|
从核心基础多线程处理文档的注册的材料。 |
|
添加了有关执行线程之间基于套接字通信的信息。 |
|
添加了示例代码,并创建和使用碳线程信息。 |
|
添加线程安全指引。 |
|
添加了有关 POSIX 线程和锁的信息。 |
|
添加了示例代码演示基于端口的通信。 |
|
此文档会替换先前发表在多线程的线程信息. |
|
2003-07-28 |
更新在第三方库的库中使用锁的建议。 |
2003-04-08 |
重申的关于锁定/解锁的平衡在第三方库。 |
2002 年 11 月 12 日 |
修订历史记录被添加到现有的主题。 |
原文地址:http://blog.csdn.net/zhaoguodongios/article/details/44081347