由于文章长度限制,本文作为[译]线程编程指南(二)后续部分。
线程安全技巧
同步工具是保证代码线程安全的有效方式,但它不是万能药。使用太多锁或者其他类型的同步原语实际上会导致应用多线程的性能反而不如非多线程时的性能。找到安全与性能之间的平衡点是一门需要经验的艺术。下列章节将为你的应用选择合适的同步等级提供帮助建议。
避免同步
对于你工作的任何新项目,甚至对现有的项目,设计代码和数据结构来避免同步使用可能是最好的解决方案。虽然锁和其他同步工具都很有用,但它们确实会影响任何应用程序的性能。如果总体设计会导致特定资源之间的高度竞争,你的线程甚至会等待更长的时间。
实现并发的最佳方法是减少并发任务之间的交互和相互依赖关系。如果每个任务都在它自己的私有数据集上运行,则不需要使用锁来保护数据。即使在两个任务共享一个共同的数据集的情况下,你也可以为每个任务提供自己的备份。当然,复制数据集也有它的成本,所以你在作出决定之前必须权衡这些成本和同步的成本。
理解同步的局限性
同步工具只有在使用多线程的应用中才会有效。如果你创建了一个互斥锁来限制某个特定资源的访问,所有的线程必须在尝试操作该资源前请求这个锁。如果不这样做,提供这样的互斥会另人困惑并成为程序猿的错误。
注意代码正确性
当使用锁技术和内存屏障技术时,你总是应该更加小心地在代码中为其提供位置。即使锁看起来实际上可以让你产生一种虚假的安全感。下面的例子将会说明这个问题,并指出在看似无害的代码中的缺陷。基本的前提是,你有一个可变数组包含一组不变的对象。假设你想调用数组中的第一个对象的方法。你可以使用下面的代码:
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[arrayLock unlock];
[anObject doSomething];
由于数组是可变的,保护数组的锁阻止了其他线程对于数组的修改直到你完成了从数组中获取到想要的对象。同时因为你获取到的对象是不可变的,所以锁就没有必要对调用doSomething
方法部分的代码进行保护。
尽管在前面的例子中存在这样一个问题。如果释放锁时另一个线程来移除数组中的所有对象,你有机会在这之前执行doSomething方法?在一个没有垃圾收集机制的应用中,代码中持有的对象可能被释放,留下一个指向无效内存地址的指针。要解决这个问题,你可以简单地重新安排你的现有代码并在调用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更好的保护数据结构完整性的方式。Volatile关键字只确保一个变量是从内存中而不是寄存器中加载,并不能确保你的代码可以正确地访问该变量。
使用原子操作
非阻塞式的同步是一种可以执行某些操作并避免锁消耗的方式。虽然锁是两个线程间一种有效的同步方式,但请求锁是资源消耗相对昂贵的操作,即便是在非冲突情况下。相反的,许多原子性操作只占用小部分时间来完成和锁同样有效的操作。
原子操作让你在32位或64位值上执行简单的数学和逻辑运算。这些操作依赖于特殊的硬件指令(和可选的内存屏障),以确保在相关的内存再次访问之前完成既定操作。在多线程的情况下,你应该经常使用包含内存障碍的原子操作以确保这部分存储在线程之间是正确同步的。
表4-3列举了可用的原子性数学和逻辑操作以及相应的函数名称。这些函数全部声明在/usr/include/libkern/OSAtomic.h头文件中,你可以在里面找到完整的语法。这些函数的64位版本只存在与64位的进程中。
表4-3 原子性的数学和逻辑操作
操作 | 函数名称 | 描述 |
---|---|---|
加(Add) | OSAtomicAdd32 OSAtomicAdd32Barrier OSAtomicAdd64 OSAtomicAdd64Barrier |
两个整型值相加并将结果赋值给指定变量。 |
递增(Increment) | OSAtomicIncrement32 OSAtomicIncrement32Barrier OSAtomicIncrement64 OSAtomicIncrement64Barrier |
指定整型值加1。 |
递减(Decrement) | OSAtomicDecrement32 OSAtomicDecrement32Barrier OSAtomicDecrement64 OSAtomicDecrement64Barrier |
指定整型值减1。 |
逻辑或(Logical OR) | OSAtomicOr32 OSAtomicOr32Barrier |
在32位值和32位掩码间执行逻辑或操作。 |
逻辑与(Logical AND) | OSAtomicAnd32 OSAtomicAnd32Barrier |
在32位值和32位掩码间执行逻辑与操作。 |
逻辑异或(Logical XOR) | OSAtomicXor32 OSAtomicXor32Barrier |
在32位值和32位掩码间执行逻辑异或操作。 |
比较和交换(Compare and swap) | OSAtomicCompareAndSwap32 OSAtomicCompareAndSwap32Barrier OSAtomicCompareAndSwap64 OSAtomicCompareAndSwap64Barrier OSAtomicCompareAndSwapPtr OSAtomicCompareAndSwapPtrBarrier OSAtomicCompareAndSwapInt OSAtomicCompareAndSwapIntBarrier OSAtomicCompareAndSwapLong OSAtomicCompareAndSwapLongBarrier |
对变量的旧值进行比较。如果两个值是相等的,这个函数将指定新值赋给该变量;否则,它什么也不做。比较和赋值作为一个原子操作,该函数会返回一个布尔值以表示是否发生交换。 |
测试和设置(Test and set) | OSAtomicTestAndSet OSAtomicTestAndSetBarrier |
在指定的变量中测试一个位,将该位设置为1,并将老位的值作为布尔值返回。位根据公式进行测试(0x80 >> (n & 7))字节((char*)address + (n >> 3)),n是位号码和地址是一个指针变量。这个公式有效地分解成8位大小的块,并在每一个块中的位顺序反转。例如,为了测试一个32位整数的最低序位(位0),你将实际指定的位号为7;同样,要测试的最高点位(位32),你将指定24位数字。 |
测试和清理(Test and clear) | OSAtomicTestAndClear OSAtomicTestAndClearBarrier |
在指定的变量中测试一个位,将该位设置为0,并将老位的值返回布尔值。位根据公式进行测试(0x80 >> (n & 7))字节((char*)address + (n >> 3)),n是位号码和地址是一个指针变量。这个公式有效地分解成8位大小的块,并在每一个块中的位顺序反转。例如,为了测试一个32位整数的最低序位(位0),你将实际指定的位号为7;同样,要测试的最高点位(位32),你将指定24位数字。 |
大多数原子函数的行为应该是相对简单并如你所期望的。然而代码4-1,显示了原子性的test-and-set以及compare-and-swap操作相对复杂的行为。前面三个调用OSAtomicTestAndSet
函数来展示位操作公式如何被用于整型值,并且其结果可能与你所期望的不同。后面两个调用展示了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的man帮助页或者/usr/include/libkern/OSAtomic.h头文件。
使用锁
锁作为线程编程的基本同步工具。锁使你能够很容易地保护大段代码,这样你就可以确保代码的正确性。OS X和iOS为所有应用类型提供了基本的互斥锁,并且Foundation Framework为特殊的情形定义了额外的变量。下面的章节将向你展示如何使用这些锁类型。
使用POSIX的Mutex锁
POSIX的互斥锁在任何应用中都能够极其简单地使用。为创建互斥锁,你需要声明并初始化一个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
@synchronized语句是Objective-C代码中创建互斥锁的便捷方式。@synchronized语句完成其他互斥锁应该做的事情-它防止不同线程在同一时间请求相同的锁。在这种情况下,你没有必要创建互斥锁或者直接锁住一个对象。相反地,你可以简单地使用任何Objective-C对象作为锁令牌,正如下面代码所示:
- (void)myMethod:(id)anObj
{
@synchronized(anObj)
{
// Everything between the braces is protected by the @synchronized directive.
}
}
传递给@synchronized语句的对象会成为区别受保护代码块的唯一标识。如果你在两个线程中执行先前的这个方法,并向每个线程传递不同的对象作为anObj参数,每个线程会获得这个锁并不受阻塞的继续执行。如果你同时传递同一个对象,其中一个线程会首先获得锁并使得另一个线程阻塞直到第一个线程退出了临界区。
作为一种预防措施,@synchronized块会向受到保护的代码隐式地添加异常处理回调。该回调在异常抛出时会自动释放互斥锁。这意味着为了使用@synchronized语句,你必须在代码中开启Objective-C的异常处理。如果你不希望由隐式异常处理程序引起额外的开销,你应该考虑使用锁类。
使用其他的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类定义了可以根据特殊值来进行锁和解锁操作的互斥锁。你不应该将该类型的锁和之前的条件量混为一谈。它和条件量某种意义上讲行为相似,但实现方式完全不同。
通常,你将NSConditionLock对象用于线程需要执行特定顺序的任务时,比如一个线程生产数据而另一个线程消费数据。当生产者执行时,消费者请求锁的条件取决于你的程序。(条件本身仅仅是一个定义的整型值)当生产者完成时,它会解锁并将锁条件置为合适的整型值来唤醒消费者线程,消费者线程然后收到并处理数据。
NSConditionLock对象中的锁定和解锁方法可以任意地组合使用。例如,你可以将锁定信息配对给unlockWithCondition:
,或者解锁信息配对给lockWithCondition:
。当然,这一组合解锁但不会释放任何线程等待特定的条件值。
下面的示例演示了如何使用条件锁处理“生产者-消费者”问题。设想应用程序包含一个数据队列。生产者线程将数据添加到队列,而消费者线程从队列中提取数据。生产者不需要等待一个特定的条件,但它必须等待锁以便它可以安全地添加数据到队列。
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线程条件量函数的简单示例。你自己的代码必须检查这些函数返回的错误码并正确地处理它们。
附录A:线程安全总结
本附录描述了OS X和iOS中某些关键框架的高级别的线程安全。本附录中的信息是随时变更的。
Cocoa
多线程中使用Cocoa的指导如下:
- 不可变(immutable)对象通常是线程安全的。一旦你创建它们,你可以在线程间安全地传递这些对象。另一方面,可变(mutable)对象通常不是线程安全的。在多线程应用中使用可变对象,应用程序必须正确同步。
- 许多对象看似“安全”实则在多线程中使用时不安全。许多这些对象可以在任何线程中使用,只要在同一时间且同一线程。被严格限制在应用程序的主线程上的对象被调用时就是如此。
- 应用程序的主线程负责处理事件。虽然其他线程进入事件路径时Application Kit将继续工作,但它的操作可发生在事件队列之外。
- 如果你想用线程来绘制视图,使用NSView的
lockFocusIfCanDraw
和unlockFocus
方法将所有绘制代码包括进来。 - 为了能在Cocoa中使用POSIX线程,你必须首先将应用置于多线程模式。
Foundation Framework线程安全
有一种误解,认为Foundation Framework是线程安全的,而Application Kit不是线程安全的。不幸的是,这只是一个总的概括,有些误导。每个框架都有线程安全的区域和线程不安全的区域。下面的章节描述了Foundation Framework的通用的线程安全性。
线程安全的类和函数
下列的类和函数通常被认为是线程安全的。你可以在多个线程中使用相同实例而不需请求锁。
NSArray
NSAssertionHandler
NSAttributedString
NSCalendarDate
NSCharacterSet
NSConditionLock
NSConnection
NSData
NSDate
NSDecimal 函数
NSDecimalNumber
NSDecimalNumberHandler
NSDeserializer
NSDictionary
NSDistantObject
NSDistributedLock
NSDistributedNotificationCenter
NSException
NSFileManager (OS X 10.5及后续版本)
NSHost
NSLock
NSLog/NSLogv
NSMethodSignature
NSNotification
NSNotificationCenter
NSNumber
NSObject
NSPortCoder
NSPortMessage
NSPortNameServer
NSProtocolChecker
NSProxy
NSRecursiveLock
NSSet
NSString
NSThread
NSTimer
NSTimeZone
NSUserDefaults
NSValue
NSXMLParser
对象的allocation 和 retain count 函数
Zone 和 memory 函数
非线程安全的类和函数
下列的类和函数通常被认为是非线程安全的。大多数情况下,你可以在多线程环境使用这些类只要你在同一时刻同一线程中。
NSArchiver
NSAutoreleasePool
NSBundle
NSCalendar
NSCoder
NSCountedSet
NSDateFormatter
NSEnumerator
NSFileHandle
NSFormatter
NSHashTable 函数
NSInvocation
NSJavaSetup 函数
NSMapTable 函数
NSMutableArray
NSMutableAttributedString
NSMutableCharacterSet
NSMutableData
NSMutableDictionary
NSMutableSet
NSMutableString
NSNotificationQueue
NSNumberFormatter
NSPipe
NSPort
NSProcessInfo
NSRunLoop
NSScanner
NSSerializer
NSTask
NSUnarchiver
NSUndoManager
User name 和 home directory 函数
请注意,尽管NSSerializer
、NSArchiver
、NSCoder
及NSEnumerator
对象自身都是线程安全的,它们被列入这里的原因是当它们包裹的数据对象被修改时是不安全的。比如,在使用归档的情况下,改变已归档的对象图是不安全的。对于枚举器,任何线程修改枚举集合是不安全的。
只能在主线程中使用的类
以下类必须仅从应用程序的主线程中使用。
NSAppleScript
可变 VS 不可变
不可变对象通常是线程安全的;一旦完成对其创建,你可以在线程间安全地传递这些对象。当然,当使用不可变对象时,你仍需要记住引用计数的正确使用。如果你不正确地释放不想保留的对象,随后也会造成异常。
可变对象通常是非线程安全的。为在多线程应用中使用可变对象,应用必须使用锁技术同步地访问它们。总之,集合类型(如NSMutableArray,NSMutableDictionary)是非线程安全的。也就是说,如果一个或多个线程正在修改同一个数组,你必须在其读写区域上锁以确保线程安全。
即便某一个方法声明返回一个不可变对象,你绝不应该简单地假设返回的对象是不可变的。取决于该方法的实现,返回的对象可能是可变的也有可能是不可变的。例如,一个本该返回NSString的方法由于其实现,可能事实上返回了一个NSMutableString。如果你想保证对象是不可变的,则必须创建一个不可变的备份。
可重入
TODO
类的初始化
Objective-C的运行时系统会在类接收其他消息前向其发送initialize
消息。这将使类在使用前有机会设置其运行时环境。在多线程应用中,运行时保证只有一个线程-这个线程恰好向类发送第一条消息,即执行initialize
方法。如果当第一个线程已经进入了initialize
方法而第二个线程试图向该类放松消息时,第二个线程会阻塞直到initialize
方法完成执行。同时,第一个线程可以继续调用该类的其他方法。initialize
方法不应该由第二个线程调用;如果这样做了,两个线程会死锁。
由于OS X 10.1.x及其早期版本存在的一个bug,线程能够在其他线程执行完initialize
方法前向类发送消息。这样一来线程会访问到并未完全初始化好的值,并可能使应用崩溃。如果你遇到这样的问题,你需要引入锁来阻止值的访问直到它们完全地被初始化或者在类变成多线程操作前强制类初始化自身。
自动释放池
每个线程都维护着自己的NSAutoreleasePool对象栈。Cocoa认为当前线程的堆栈中总是有一个可用的自动释放池。如果一个池不可用,对象不被释放并导致内存泄漏。在基于Application Kit的应用主线程中,其NSAutoreleasePool对象会自动创建和销毁,但辅助线程(和仅使用Foundation的应用)在使用Cocoa前必须自己创建。如果你的线程是长期运行的且潜在地生成了大量的自动释放对象,你应该周期性地销毁和创建自动释放池(如Application Kit在主线程中所做一样);否则,自动释放对象的积累并导致内存的增长。如果你的分离线程不使用Cocoa,则不需要创建一个自动释放池。
Run Loops
每个线程有且仅有一个run loop。每个run loop,都有自己的一系列模式来决定哪一个输入源被监听。Run loop中定义的模式不受其他run loop模式的影响,即便它们有相同的名称。
如果你的应用基于Application Kit主线程的run loop将自动运行,但是辅助线程(和仅使用Foundation的应用)必须自己启动run loop。如果分离不进入run loop,在其方法执行完毕后线程会立即退出。
虽然出于一些外部因素,NSRunLoop类并不是线程安全的,你只应该从持有它的线程中使用该类的实例方法。
Application Kit 框架线程安全
下面部分描述了Application Kit框架中常用的线程安全内容。
非线程安全类
下列的类和函数通常是非线程安全你的。在大多数情况下,你可以在多线程环境下使用它们,仅当同一时刻同一线程时。
- NSGraphicsContext。
- NSImage。
- NSResponder。
- NSWindow及其所有的子类。
只能在主线程中使用的类
下列的类只能用于应用的主线程中。
- NSCell及其子类。
- NSView及其子类。
窗口限制
你可以在辅助线程上创建窗口。Application Kit可以确保与窗口关联的数据结构在主线程上被销毁以防止竞态情况发生。如果应用程序同时处理大量的窗口,也存在窗口对象内存泄漏的可能。
你可以在辅助线程上创建一个模态的窗口。当主线程运行在run loop的模态模式下时,Application Kit会阻塞辅助线程的调用。
事件处理限制
应用的主线程负责处理事件。主线程被NSApplication的run
方法调用时阻塞,在应用的main
函数中调用。虽然其他线程进入事件路径时Application Kit将继续工作,但它的操作可发生在事件队列之外。例如,如果两个线程同时响应一个关键事件,事件会被乱序接收。让主线程处理事件,可以带来一致性的用户体验。一旦收到事件,事件会被分发到辅助线程以供后续处理。
你可以从辅助线程调用NSApplication的postEvent:atStart:
方法来向主线程的事件队列推送事件。然而,由于用户输入的事件不同顺序并不能够得到保障。应用的主线程仍会负责处理事件队列中的事件。
图形绘制限制
Application Kit中使用图形相关的类和函数绘图通常是线程安全的,包括NSBezierPath和NSString类。使用特定类的细节在下面部分将会描述。
- NSView限制
TODO - NSGraphicsContext限制
TODO - NSImage限制
TODO
Core Data 框架线程安全
Core Data框架支持多线程,尽管其中有些注意事项。获取更多相关注意事项,请查看《Core Data Programming Guide》。
Core Foundation
Core Foundation足够的线程安全,如果程序中加以小心,你应该不会陷入任何线程冲突的问题。通常情况下它都是线程安全的,比如说查询、保留、释放或者传递不可变的对象。即便多个线程对共享的对象进行请求,它都是可靠的线程安全。
类似Cocoa,Core Foundation遭遇对象及对象内部的变化时变得线程不安全的情况。例如,正如你所预期的那样,修改可变数据或可变数组对象就是非线程安全的,修改不可变数组中的对象时也是如此。出于性能因素考虑,在这些情况下是至关重要的。此外,在该层级通常是不可能实现绝对的线程安全。你不能排除,如保持一个从集合中获得的对象导致的不确定行为。集合本身可以在调用保留所包含的对象之前被释放。
在这些情况下,从多线程中访问Core Foundation对象,你的代码应该防止以锁的方式同时访问。例如,代码枚举了一个Core Foundation数组中的对象,应使用适当的锁定调用该枚举块以防止数组被其他线程改变。