锁是线程编程中非常基本的同步工具。它可以很轻松地确保我们大部分代码能够以正确方式执行。OS X和iOS提供基本的互斥锁,Foundation框架还为特殊情况定义互斥锁的变体。下面几节会展示如何使用这几种锁。

4.6.1 使用POSIX互斥锁

在任何程序中使用POSIX互斥锁都是相当简单的。为了创建一个互斥锁,我们需要声明一个pthread_mutex_t的结构变量。同时我们使用pthread_mutex_lockpthread_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),该协议定义了lockunlock方法。我们可以使用这些方法来获取和释放锁(这和互斥锁差不多)。

除了标准的加锁行为(lockunlock)以外,NSLock类还提供tryLocklockBeforeDate:方法。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对象的加锁解锁方法可以有多种组合。比如lockunlockWithCondition: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方法;

results matching ""

    No results matching ""