iOS 各种锁

作者: NapoleonY | 来源:发表于2018-07-25 12:22 被阅读95次

概述

iOS多线程开发,会出现数据竞争,因此需要锁来保证线程安全。


iOS锁

线程安全

当一个线程访问资源时,需要保证其它的线程不能访问同一资源,即同一时刻只能有一个线程对数据进行操作。因此需要锁来保证线程的安全。

锁的使用步骤

  1. 准备一把锁:递归锁、互斥锁、条件锁、信号量等等
  2. 线程1中:
    • 加锁(- (void)lock;
    • 处理数据
    • 解锁(- (void)unlock;
  3. 线程2中:
    • 等待锁在线程1中使用完毕,并解锁
    • 加锁(- (void)lock;
    • 处理数据
    • 解锁(- (void)unlock;

常用的锁

开发中常用如下几种锁

NSRecursiveLock:递归锁

NSLock多次 lock却没有unlock会导致死锁,例如下列情形,在递归调用中,会出现死锁。

self.myLock = [NSLock new];
dispatch_async(self.queue_1, ^{
        static void (^recursiveBlock)(int);
        recursiveBlock = ^(int value) {
            [self.myLock lock];
            if (value > 0) {
                recursiveBlock(value - 1);
            }
            [self.myLock unlock];
        };
        recursiveBlock(10);
    });

因此有了递归锁(NSRecursiveLock),将上述代码中的NSLock换成NSRecursiveLock即可解决问题。递归锁其他用法与NSLock一致。如下代码所示

dispatch_async(self.queue_1, ^{
        static void (^recursiveBlock)(int);
        recursiveBlock = ^(int value) {
            [self.recursiveLock lock];
            if (value > 0) {
                recursiveBlock(value - 1);
            }
            [self.recursiveLock unlock];
        };
        recursiveBlock(10);
    });
    dispatch_async(self.queue_2, ^{
        BOOL x = [self.recursiveLock lockBeforeDate:[NSDate distantFuture]];
        if (x) {
            [self.recursiveLock unlock];
        } else {
            NSLog(@"线程__2:__获取__锁__失败");
        }
    });
NSConditionLock:条件锁

NSConditionLock相比于NSLock多了一个条件
当两个线程需要根据特定的条件或者按照特定的顺序执行时,就需要NSConditionLock。例如代码中开启了线程一下载图片,线程二处理图片。只有线程一下载图片完成后,线程二才能开始处理图片。在线程一下载完成之前,线程二处于阻塞状态。

self.conditionLock = [[NSConditionLock alloc] initWithCondition:kConditionOne];
dispatch_async(self.queue_1, ^{
        [self.conditionLock lockWhenCondition:kConditionOne];
        sleep(5);
        [self.conditionLock unlockWithCondition:kConditionTwo];
    });
    dispatch_async(self.queue_2, ^{
        [self.conditionLock lockWhenCondition:kConditionTwo];
        sleep(5);
        [self.conditionLock unlock];
    });
  • 条件锁在初始化的时候,设定了一个条件kConditionOne
  • [self.conditionLock unlockWithCondition:kConditionTwo];解锁时重新给NSConditionLock设定了一个条件为kConditionTwo
  • 当满足条件kConditionTwo时,可以重新获取这把锁
  • 最后不需要更改获取锁的条件了,直接解锁

NSCondition

NSCondition是一种特殊的锁,与NSConditionLock类似,但是实现方式不一样。

dispatch_async(self.queue_1, ^{
        NSLog(@"线程__1:__准备获取__锁__");
        [self.myCondition lock];
        NSLog(@"线程__1:__获取__锁__成功,并开始等待");
        [self.myCondition wait];
        NSLog(@"线程__1:__结束等待");
        [self.myCondition unlock];
        NSLog(@"线程__1:__解__锁__成功");
    });
    dispatch_async(self.queue_2, ^{
        NSLog(@"线程__2:__准备获取__锁__");
        [self.myCondition lock];
        NSLog(@"线程__2:__获取__锁__成功,并开始等待");
        [self.myCondition signal];
        NSLog(@"线程__2:__发出信号");
        [self.myCondition unlock];
        NSLog(@"线程__2:__解__锁__成功");
    });

上述代码运行结果


NSCondition

其中

  • - (void)wait;会阻塞当前线程
  • - (void)signal;激活一个阻塞的线程
  • - (void)broadcast;激活所有阻塞的线程
    备注:
    • 通过测试发现,- (void)signal;按照调用- (void)wait;方法先后顺序激活线程,并不是首先激活与调用- (void)signal;方法的线程
    • 可以多次调用- (void)signal;方法依次激活被- (void)wait;阻塞的线程
dispatch_semaphore:信号量

信号量类似于自动取款机。一次只能有一个人使用取款机。如果来的人多了,只能在旁边等着,如果使用取款机的人办完业务了,下一个人才能继续使用。

  • dispatch_semaphore_create(1)传入值需>=0,若传入0,则阻塞线程
  • dispatch_semaphore_wait(semaphore, timeout);,等待timeout,到了时间后会继续执行代码;或者信号量semaphore大于0也会继续执行代码
  • dispatch_semaphore_signal(signal);类似于unlock,信号量会+1
    信号量原理:首先把信号量减一,如果不小于零,就立刻返回,否则就使线程睡眠,让出时间片。主动让出时间片会导致操作系统切换到另一个线程,会花费10us左右的时间,并且需要切换两次,因此如果忙等时间只有几微秒,忙等比线程睡眠更高效。

如下述代码所示。

dispatch_semaphore_t signal = dispatch_semaphore_create(1);

dispatch_queue_t queue1 = dispatch_queue_create("globalQueue1", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue1, ^{
    NSLog(@"线程1:等待");
    dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
    NSLog(@"线程1:启动");
    dispatch_semaphore_signal(signal);
    NSLog(@"线程1:新的信号");
});

实例:有三个任务异步A、B、C,其中需要在A、B执行完毕后才可以执行任务C
可以使用信号量解决:

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semphore = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
    sleep(2);
    NSLog(@"任务A执行");
    dispatch_semaphore_signal(semphore);
});
dispatch_async(queue, ^{
    sleep(3);
    NSLog(@"任务B执行");
    dispatch_semaphore_signal(semphore);
    });
dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER); dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER);
NSLog(@"C执行等待");
POSIX互斥锁

POSIX互斥锁是Linux/Unix平台上提供的API,C语言级别的锁,使用POSIX互斥锁需要引入头文件#import <pthread.h>,并申明初始化一个pthread_mutex_t的结构。使用完毕,需要在- (void)dealloc;中释放锁。

  • pthread_mutex_lock加锁
  • pthread_mutex_unlock解锁
  • pthread_mutex_destroy释放锁
    原理:pthread_mutex与信号量原理类似,不使用忙等,而是阻塞线程并睡眠,需要上下文切换,有多种类型,可通过定义锁的属性PTHREAD_MUTEX_NORMALPTHREAD_MUTEX_ERRORCHECKPTHREAD_MUTEX_RECURSIVE确定类型。
    互斥锁内部会首先判断锁的类型,所以效率相对于信号量会低一些。
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);  // 定义锁的属性
    
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr); // 创建锁

示例如下

dispatch_async(self.queue_1, ^{
        NSLog(@"线程__1:__准备获取__锁__");
        pthread_mutex_lock(&_mutex);
        NSLog(@"线程__1:__获取__锁__成功");
        sleep(5);
        pthread_mutex_unlock(&_mutex);
        NSLog(@"线程__1:__解__锁__成功");
    });
    dispatch_async(self.queue_2, ^{
        NSLog(@"线程__2:__准备获取__锁__");
        pthread_mutex_lock(&_mutex);
        NSLog(@"线程__2:__获取__锁__成功");
        pthread_mutex_unlock(&_mutex);
        NSLog(@"线程__2:__解__锁__成功");
    });
pthread_mutex_destroy(&_mutex);

结果如下

POSIX互斥锁
备注:POSIX提供了一整套完整的API,功能强大,非常底层。
NSLock:最基本的锁
  • - (void)lock;
  • - (void)unlock;
  • - (BOOL)lockBeforeDate:(NSDate *)limit;在limit时间内尝试获取锁,为获取锁前一直阻塞线程,例如10s,如果10s内的时间,其它线程释放了锁(unlock),该方法会立刻获取锁
    原理:NSLock内部封装了属性为PTHREAD_MUTEX_ERRORCHECKpthread_mutex,由于存在方法调用,因此会比pthread_mutex更慢
    注意:
    1. lockunlock方法必须在同一线程中执行
    2. 连续lock中间没有unlock会引起死锁
dispatch_async(self.queue_1, ^{
        NSLog(@"线程1:等待");
        [self.myLock lock];
        NSLog(@"线程1");
        sleep(5);
        [self.myLock unlock];
        NSLog(@"线程1:解锁成功");
});

dispatch_async(self.queue_2, ^{
        NSLog(@"线程2:等待");
        BOOL x = [self.myLock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:6]];
        //BOOL x = [self.myLock lockBeforeDate:[NSDate distantFuture]];
        if (x) {
            NSLog(@"线程2:成功");
            [self.myLock unlock];
        } else {
            NSLog(@"线程2:失败");
        }
});
@synchronized

接触较早的线程锁,具体代码如下

dispatch_async(self.queue_1, ^{
        NSLog(@"synch__线程__1:__准备获取__锁__");
        @synchronized(self) {
            NSLog(@"synch__线程__1:__获取__锁__成功");
            sleep(5);
        }
        NSLog(@"synch__线程__1:__解__锁__成功");
    });
    dispatch_async(self.queue_2, ^{
        NSLog(@"synch__线程__2:__准备获取__锁__");
        @synchronized(self) {
            NSLog(@"synch__线程__2:__获取__锁__成功");
        }
        NSLog(@"synch__线程__2:__解__锁__成功");
    });

结果:


synchronized
OSSpinLock:自旋锁

由于OSSpinLock存在优先级反转问题,在iOS 10.0被os_unfair_lock代替了


os_unfair_lock

在iOS 10.0后可用,用于代替自旋锁。使用时需要引入头文件#import <os/lock.h>

static os_unfair_lock unfairLock;
unfairLock = OS_UNFAIR_LOCK_INIT;
dispatch_async(self.queue_1, ^{
        NSLog(@"unfairLock__线程__1:__准备获取__锁__");
        os_unfair_lock_lock(&unfairLock);
        NSLog(@"unfairLock__线程__1:__获取__锁__成功");
        sleep(5);
        os_unfair_lock_unlock(&unfairLock);
        NSLog(@"unfairLock__线程__1:__解__锁__成功");
});
dispatch_async(self.queue_2, ^{
        NSLog(@"unfairLock__线程__2:__准备获取__锁__");
        os_unfair_lock_lock(&unfairLock);
        NSLog(@"unfairLock__线程__2:__获取__锁__成功");
        os_unfair_lock_unlock(&unfairLock);
        NSLog(@"unfairLock__线程__2:__解__锁__成功");
});
结果

性能分析

,性能如下:

  1. 测试环境:iPhone SE,iOS 10.0.1


    iPhone SE上锁性能定性分析
  2. 测试环境:iPhone 7, iOS 11.4


    iPhone 7上锁性能分析

上述图表是在单线程下测试的,并且只测试了加锁、解锁的性能,因此只能做定性分析。
从图表中可以看出,dispatch_semaphore性能最好,p thread_mutex、os_unfair_lock、NSCondition性能相近,pthread_mutex_recursive、NSRecursiveLock、NSLock性能次之,然后是NSConditionLock,synchronized性能最差。

未完待续

参考

  1. iOS 开发中的八种锁(Lock)
  2. 深入理解 iOS 开发中的锁
  3. iOS多线程-各种线程锁的简单介绍

相关文章

网友评论

    本文标题:iOS 各种锁

    本文链接:https://www.haomeiwen.com/subject/edgumftx.html