iOS 各种锁

概述

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


iOS锁

线程安全

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

锁的使用步骤

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

从原理上讲锁分为哪几种?

  1. 自旋锁(叫循环判断锁更恰当一些)

    已加锁时一直循环处于判断 lock 状态,如果 lock = 0 接着循环,如果 lock = 1 跳出循环转去临界区。因此,如果别的线程长期持有该锁,那么这个线程就一直在 while 地检查是否能够加锁,浪费 CPU 做无用功;如果拥有锁后很快释放锁,那么自旋锁就比较高效。一般用于多核 CPU

    while 抢锁(lock) == 没抢到 {
    }
    
  2. 互斥锁

    已加锁时阻塞掉当前线程,让出 CPU 资源,通过减少 CPU 的浪费来提高效率。操作系统负责线程调度,为了实现“锁的状态发生改变时再唤醒”,就需要把锁也交给操作系统管理。所以这个过程需要进行上下文的切换,保存寄存器状态需要花费时间,操作花销较自旋锁更大

    while 抢锁(lock) == 没抢到 {
     本线程先去睡了,请在这把锁的状态发生改变时再唤醒
    }
    
  3. 读写锁(共享-独占锁)

    是一种读共享,写独占的锁。主要有两种特性:

    • 当读写锁被加了写锁时,其他线程对该锁加度锁或者写锁都会阻塞(不是失败)
    • 当读写锁被加了读锁时,其他线程对该锁加写锁会阻塞,加读锁会成功

iOS 开发中常用的几种锁分别属于哪种类型?

自旋锁
  • os_unfair_lock

  • OSSPinLock:iOS 10.0 以后被弃用

互斥锁
  • pthread_mutex:有 PTHREAD_MUTEX_NORMALPTHREAD_MUTEX_ERRORCHECKPTHREAD_MUTEX_RECURSIVE 三种类型
  • NSLock:对 mutex 普通锁的封装
  • NSRecursiveLock:对 mutex 递归锁的封装
  • NSCondition:对 mutex 和 条件的封装
  • NSConditionLock: 对 NSCondition 的进一步封装,可以设置具体的条件
  • 信号量 dispatch_semaphore:如果信号量不小于 0,立刻返回,否则是线程睡眠,让出时间片,导致操作系统切换到另一个线程,会花费 10us左右时间,并且需要切换两次
  • @synchronized:是对 mutex 递归锁的封装,@sychronized(obj) 内部会生成 obj 对应的递归锁,然后进行加锁、解锁操作
  • atomic:用于保证属性的 setter、getter 的原子属性操作,相当于在 getter 和 setter 内部加了线程同步的锁。它不能保证使用属性的过程是线程安全的
读写锁
  • pthread_rwlock:需要注意以下场景
    1. 同一时间,只能有 1 个线程进行写操作
    2. 同一时间,允许有多个线程进行多操作
    3. 同一时间,不允许既有写操作,又有读操作

常用的锁

开发中常用如下几种锁

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_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

具体可以参考关于@synchronized 比你想知道的还多。@synchronized(objc)会在运行时为 objc 分配递归锁,如果objc 为nil,则代码失去了线程安全性,如果在 @synchronized(objc)中 objc 惜构,也不会产生问题
具体代码如下

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

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多线程-各种线程锁的简单介绍
  4. iOS多线程安全-13种线程锁

你可能感兴趣的:(iOS 各种锁)