锁是线程编程中非常基本的同步工具。它可以很轻松地确保我们大部分代码能够以正确方式执行。OS X和iOS提供基本的互斥锁,Foundation框架还为特殊情况定义互斥锁的变体。下面几节会展示如何使用这几种锁。
4.6.1 使用POSIX互斥锁
在任何程序中使用POSIX互斥锁都是相当简单的。为了创建一个互斥锁,我们需要声明一个pthread_mutex_t
的结构变量。同时我们使用pthread_mutex_lock
和pthread_mutex_unlock
函数来为互斥锁做加锁或者解锁的操作。下面代码展示使用posix互斥锁的基本代码。当我们使用完该锁之后,简单的调用pthread_mutex_destroy
函数释放该锁的相关数据。
/// 官方文档例子
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
/// do work
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
上面函数的返回中是返回int型值,我们在实际程序中需要对这些状态码做相应的操作。
4.6.2 使用NSLock类
NSLock为Cocoa程序实现的基本互斥锁。锁的所有接口都遵循了NSLocking协议(包括NSLock
),该协议定义了lock
和unlock
方法。我们可以使用这些方法来获取和释放锁(这和互斥锁差不多)。
除了标准的加锁行为(lock
和unlock
)以外,NSLock类还提供tryLock
和lockBeforeDate:
方法。tryLock
方法用于判断当前锁是否可用。lockBeforeDate:
方法会在指定时间内去加锁,如果加锁失败则会解除当前阻止的线程(并返回NO)。
/// 官方文档例子
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];
}
}
4.6.3 使用@synchronized
@synchronized
指令是objective-c中创建互斥锁的一个便捷方式。@synchronized
会和任何其他的锁互斥(它可以防止不同线程同一时间获取同一个锁)。在这种情况下我们不必去创建互斥锁或者其他的锁对象,相反,只需要使用Objective-C对象来作为锁的token,就像下面这样:
- (void)myMethod:(id)anObj{
@synchronized(anObj){
}
}
传递给@synchronized
指令的对象是用于区分被保护block的唯一标识符。如果在两个不同线程执行上诉的方法,只需要在不同线程上的方法中传递不同的token值就可以了,每一个都是在自己线程做相关处理,而不会被对方阻塞。如果传递的是同一个token,其中一个线程会先处理,而另一个线程会等到前一个线程处理完成之后再处理。
同时@synchronized
块中还添加了隐式的异常处理,在抛出异常时程序会自动释放互斥锁。在使用@synchronized
时我们必须要在Objective-C代码中开启异常处理功能。相反,如果不想使用隐式异常处理功能(因为这会引起额外的开销),那么就放弃该指令选择使用锁。
4.6.4 使用其他Cocoa锁
下面介绍Cocoa中其他几种类型的锁。
4.6.4.1 使用NSRecursiveLock对象(递归锁)
NSRecursiveLock
类定义了一个可以被同一个线程获取多次但不会造成deadlock(死锁)的锁。递归锁会跟踪它被成功获取的次数。每成功的获取一次锁都需要调用相应的unlock来解锁,只有当所有的加锁和解锁操作一一对应之后才会释放掉该锁,此时其他线程便可以获取它。
⚠️ 递归锁使用场景:顾名思义,递归锁用于递归函数内部,防止递归阻塞线程。
同样可以用于非递归函数场景,下面的例子是一个简单的递归函数使用递归锁的例子,在这个例子中如果没有NSRecursiveLock
对象的话,再次调用这个函数的话就会造成死锁。
/// 官方文档例子
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
void MyRecursiveFunction(int value)
{
[theLock lock];
if (value != 0)
{
--value;
MyRecursiveFunction(value);
}
[theLock unlock];
}
MyRecursiveFunction(5);
⚠️ ⚠️ ⚠️ :使用递归锁,一定要记住加锁和解锁次数一定要平衡,否则会造成无法释放锁。
4.6.4.2 使用NSConditionLock对象
NSConditionLock
对象作为一个互斥锁,它可以根据特定值来加锁或者解锁(这个特定值就可以理解为某个条件,比如某某值等于另外一个值之类的)。不能根据名字认为这是关于条件(condition)某一类型的锁。在某些方面确实和条件有点相似,但是实现方式是有很大不同的。
通常,当线程需要执行任务时,我们可以在指定代码中使用NSConditionLock
对象,比如当一个线程(producer thread)创建了供另外线程(consumer thread)使用的数据时。当producer thread开始执行时,consumer thread通过程序中特定的条件来获取锁对象(这个条件是我们自己来定义的)。在producer thread将相关数据创建完成之后,程序会解锁并将该锁的条件设置为相应的值来唤consumer thread,然后由它来处理数据。
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.
}
4.6.4.3 使用NSDistributedLock对象
NSDistributedLock类可用于多个主机上多个应用程序,以限制对多程序共享资源(如文件)的访问。该锁本身就是一个有效的互斥锁,通过文件系统(比如文件或者目录)实现。在使用NSDistributedLock
对象时,该锁对象在使用它的程序中必须要是可写的。它通常是在计算机中所有程序都可以访问的文件系统上。
和其他类型的锁不一样,NSDistributedLock并没有继承NSLocking协议,因为并没有lock方法。lock方法会阻塞线程的执行,系统会以预定速率来轮询锁对象。NSDistributedLock提供了tryLock方法让你决定是否需要去轮询。
由于是使用文件系统实现,NSDistributedLock对象在没有被持有者明确释放的情况下是不会被释放的。如果程序是由于持有distributed锁崩溃的话,其他clients就不能去获取被保护的资源。在这种情况下,我们可以用breakLock方法来破坏掉现有的锁来获取它。应该尽可能的避免破坏锁,除非你确定你的进程在无法释放锁的情况下崩溃。
和其他锁一样,当使用完了NSDistributedLock对象,我们可以使用unlock方法来释放它。
本人总结TIPS
- 在使用posix互斥锁时,当使用完该锁之后,我们需要调用
pthread_mutex_destroy
函数来释放该锁的相关数据; - NSLock除了提供基本的lock和unlock之外,还提供了tryLock和lockBeforeDate:方法;
- 传递给@synchronized指令的对象是用于区分被保护block的唯一标识符;
@synchronized块中还添加了隐式的异常处理,在抛出异常时程序会自动释放互斥锁;
递归锁用于递归函数内部;
递归锁每成功的获取一次锁都需要调用相应的unlock来解锁;
使用递归锁,一定要记住加锁和解锁次数一定要平衡,否则会造成无法释放锁。
条件锁一般用于生产-消费模型中;
NSDistributedLock并没有继承NSLocking协议,因为并没有lock方法;