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