使用锁
锁是用于线程编程的基本同步工具。锁使你可以轻松保护大部分代码,从而可以确保该代码的正确性。OS X 和 iOS 为所有应用程序类型提供基本互斥锁,并且 Foundation 框架为特殊情况定义了互斥锁的一些其他变体。以下各节说明如何使用这些锁类型中的几种。
使用 POSIX 互斥锁
POSIX 互斥锁非常易于在任何应用程序中使用。要创建互斥锁,声明并初始化pthread_mutex_t结构。要锁定和解锁互斥锁,使用pthread_mutex_lock和pthread_mutex_unlock函数。下面展示了初始化和使用 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);
}
注意:代码是一个简化的示例,旨在显示 POSIX 线程互斥函数的基本用法。你的代码应检查这些函数返回的错误代码并进行适当处理。
【译者注:POSIX 互斥锁在 YYKit 有用到,比如在 YYMemoryCache 中。使用方式:
@implementation YYMemoryCache {
pthread_mutex_t _lock;
...
}
- (void)someMethod {
pthread_mutex_lock(&_lock);
...
pthread_mutex_unlock(&_lock);
}
】
使用 NSLock 类
NSLock对象为 Cocoa 应用程序实现了基本的互斥锁。实际上,所有锁(包括NSLock)的接口都是由NSLocking协议定义的,该协议定义了lock和unlock方法。你可以像使用任何互斥锁一样使用这些方法来获取和释放锁。
除了标准的锁定行为之外,NSLock类还添加了tryLock和lockBeforeDate:方法。如果锁不可用, tryLock方法尝试获取锁,但不会阻塞,仅返回NO。如果未在指定的时间限制内获取锁,lockBeforeDate:方法尝试获取锁,但不阻塞线程(并返回NO)。【原文:In addition to the standard locking behavior, the NSLock class adds the tryLock and lockBeforeDate: methods. The tryLock method attempts to acquire the lock but does not block if the lock is unavailable; instead, the method simply returns NO. The lockBeforeDate: method attempts to acquire the lock but unblocks the thread (and returns NO) if the lock is not acquired within the specified time limit. 这个地方有点糊涂】
下面的示例演示如何使用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];
}
}
NSLock 在 AFNetworking 中有使用。使用方式:
static NSLock* imageLock = nil;
@implementation UIImage (AFNetworkingSafeImageLoading)
+ (UIImage *)af_safeImageWithData:(NSData *)data {
UIImage* image = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
imageLock = [[NSLock alloc] init];
});
[imageLock lock];
image = [UIImage imageWithData:data];
[imageLock unlock];
return image;
}
使用 @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 异常处理。如果你不希望由隐式异常处理程序引起的额外开销,则应考虑使用锁类。
有关@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对象定义一个互斥锁,该互斥锁可以使用特定值进行锁定和解锁。你不应将这种类型的锁与条件(Contidion)混淆(请参阅条件)。该行为在某种程度上类似于条件,但实现方式却大不相同。
通常,当线程需要以特定顺序执行任务时(例如,当一个线程产生另一个线程消耗的数据时),你可以使用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.
}
【译者注:NSConditionLock 在 PINCache 中有使用。使用方式:
@interface PINDiskCache () {
NSConditionLock *_instanceLock;
}
- (void)lock {
[_instanceLock lockWhenCondition:PINDiskCacheConditionReady];
}
- (void)unlock {
[_instanceLock unlockWithCondition:PINDiskCacheConditionReady];
}
#pragma mark - Public Thread Safe Accessors
- (PINDiskCacheObjectBlock)willAddObjectBlock {
PINDiskCacheObjectBlock block = nil;
[self lock];
block = _willAddObjectBlock;
[self unlock];
return block;
}
其实我觉得这些库里使用的野蛮随意的,我看不出来这几个场景下,一定要使用某种锁或者不能使用某种锁的理由。 】
【译者注:上述各种锁类的定义以及 NSLocking Protocol 的定义都在 NSLock.h 当中】
使用NSDistributedLock对象
NSDistributedLock类可被多个主机上的多个应用程序用来限制对某些共享资源(例如文件)的访问。该锁本身实际上是使用文件系统项(如文件或目录)实现的互斥锁。为了使NSDistributedLock对象可用,使用该锁的所有应用程序都必须可写该锁。这通常意味着将其放置在运行该应用程序的所有计算机都可以访问的文件系统上。
与其他类型的锁不同,NSDistributedLock不符合NSLocking协议,因此没有锁定方法。锁定方法将阻止线程的执行,并要求系统以预定速率轮询锁定。 NSDistributedLock不会将这种惩罚加在你的代码上,而是提供一个tryLock方法并让您决定是否进行轮询。
因为它是使用文件系统实现的,所以除非所有者明确释放,否则不会释放NSDistributedLock对象。如果你的应用程序在持有分布式锁的同时崩溃,则其他客户端将无法访问受保护的资源。在这种情况下,可以使用breakLock方法来破坏现有锁,以便获取它。但是,通常应该避免破坏锁,除非你确定拥有进程已死并且无法释放锁。
与其他类型的锁一样,使用NSDistributedLock对象完成操作后,可以通过调用unlock方法来释放它。
使用条件
条件是一种特殊类型的锁,可用于同步操作必须执行的顺序。它们与互斥锁有一个微妙的区别。等待某个条件的线程保持阻塞状态,直到该条件被另一个线程显式发出信号为止。
由于实现操作系统所涉及的微妙之处,即使代码未真正发出条件锁,也允许伪造成功返回条件锁。为了避免由这些虚假信号引起的问题,你应始终将谓词与条件锁结合使用。谓词是确定线程继续执行是否安全的更具体方法。该条件只是让你的线程处于睡眠状态,直到可以由信令线程设置谓词为止。
以下各节说明如何在代码中使用条件。
使用NSCondition类
NSCondition类提供的语义与POSIX条件相同,但是将所需的锁和条件数据结构都包装在一个对象中。结果是可以像互斥锁一样锁定对象,然后像条件一样等待。
下面显示了一个代码片段,演示了等待NSCondition对象的事件序列。 cocoaCondition变量包含一个NSCondition对象,并且timeToDoWork变量是一个整数,在发出该信号之前立即从另一个线程递增。
[cocoaCondition lock];
while (timeToDoWork <= 0)
[cocoaCondition wait];
timeToDoWork--;
// Do real work here.
[cocoaCondition unlock];
下面代码用于表示 Cocoa 条件并增加谓词变量的代码。你应该始终在发出信号之前锁定该条件。
[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];
使用 POSIX 条件
POSIX 线程条件锁要求同时使用条件数据结构和互斥锁。尽管两个锁结构是分开的,但互斥锁在运行时与条件结构密切相关。等待信号的线程应始终一起使用相同的互斥锁和条件结构。更改配对会导致错误。
下面显示了条件和谓词的基本初始化和用法。在初始化条件和互斥锁之后,等待线程使用ready_to_go变量作为其谓词进入while循环。仅当谓词已设置且随后发出条件通知时,等待线程才会唤醒并开始执行其工作。
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);
}
信号线程既负责设置谓词,也负责将信号发送到条件锁。下面显示了实现此行为的代码。在此示例中,条件在互斥锁内部发出信号,以防止在等待条件的线程之间发生竞争条件。
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 线程条件函数的基本用法。你自己的代码应检查这些函数返回的错误代码并进行适当处理。