iOS探索 -- iOS中的锁(二)

109 阅读6分钟

iOS 探索系列相关文章 :

iOS 探索 -- alloc、init 与 new 的分析

iOS 探索 -- 内存对齐原理分析

iOS 探索 -- isa 的初始化和指向分析

iOS 探索 -- 类的结构分析(一)

iOS 探索 -- 类的结构分析(二)

iOS 探索 -- 消息查找流程(一)

iOS 探索 -- 消息查找流程(二)

iOS 探索 -- 动态方法决议分析

iOS 探索 -- 消息转发流程分析

iOS 探索 -- 离屏渲染

iOS 探索 -- KVC 原理分析

iOS 探索 -- iOS中的锁(一)

iOS 探索 -- iOS中的锁(二)

接上文对 iOS 中的一些锁来进行分析, 前面主要分析了 iOS 中的 @synchronized 锁的实现和相关问题, 接下来对其他的一些锁来进行分析

1. NSLock

1. 实现分析

NSLock 在开发中也算是比较常见的锁了, 它是一个 非递归互斥锁

 @protocol NSLocking
 - (void)lock; // 加锁
 - (void)unlock; // 解锁
 @end
 ​
 @interface NSLock : NSObject <NSLocking> {
 @private
     void *_priv;
 }
 ​
 - (BOOL)tryLock; // 尝试加锁
 - (BOOL)lockBeforeDate:(NSDate *)limit; // 带着截止日期的加锁
 // 锁的名字
 @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
 ​
 @end

NSlock 的底层实现是在 Foundation 框架下的, 没有开源; 但是 SwiftFoundation 已经开源了, 所以可以大致了解一下先:

 #if os(Windows)
 private typealias _MutexPointer = UnsafeMutablePointer<SRWLOCK>
 private typealias _RecursiveMutexPointer = UnsafeMutablePointer<CRITICAL_SECTION>
 private typealias _ConditionVariablePointer = UnsafeMutablePointer<CONDITION_VARIABLE>
 #elseif CYGWIN
 private typealias _MutexPointer = UnsafeMutablePointer<pthread_mutex_t?>
 private typealias _RecursiveMutexPointer = UnsafeMutablePointer<pthread_mutex_t?>
 private typealias _ConditionVariablePointer = UnsafeMutablePointer<pthread_cond_t?>
 #else
 private typealias _MutexPointer = UnsafeMutablePointer<pthread_mutex_t>
 private typealias _RecursiveMutexPointer = UnsafeMutablePointer<pthread_mutex_t>
 private typealias _ConditionVariablePointer = UnsafeMutablePointer<pthread_cond_t>
 #endif

在源码里面可以看到有 mutex 互斥锁相关的字眼, 这里就不过多研究了, 有兴趣的可以自行了解一下, 接下来来看看它的使用方面的一些东西。

2. 使用

1. 正常使用
 - (void)nslock_Text {
     NSLock *lock = [[NSLock alloc] init];
     
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         [lock lock];
         NSLog(@"线程 1 开始");
         sleep(5);
         NSLog(@"线程 1 完成");
         [lock unlock];
     });
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         [lock lock];
         NSLog(@"线程 2 开始");
         sleep(2);
         NSLog(@"线程 2 完成");
         [lock unlock];
     });
 }
 // 结果
     线程 1 开始
     线程 1 完成
     线程 2 开始
     线程 2 完成

线程 1 持有锁开始执行, 线程 2 此时就无法继续持有锁了进入休眠状态, 等待线程 1 执行完毕以后线程 2 唤醒后持有锁开始执行。

2. 某些场景存在的问题
 - (void)test {
     NSLock *lock = [[NSLock alloc] init];
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         static void (^block)(int);
         
         block = ^(int value) {
             NSLog(@"加锁 --------");
             [lock lock];
             NSLog(@"加锁成功 ----");
             if (value > 0) {
                 NSLog(@"value = %d", value);
                 block(value - 1);
             }
             NSLog(@"解锁 --------");
             [lock unlock];
         };
         block(10);
     });
 }
 // 执行结果
     加锁 -------
     加锁成功 ----
     value = 10
     加锁 -------

看到上面的结果了吗, 第一次加锁加锁成功后在 block 块内进行了递归操作, 递归操作再次来到加锁时因为前面的过程还没有解锁, 所以就不能再次加锁成功, 就导致了线程堵塞。

出现问题的原因就是因为 NSLock 是一个 非递归锁 , 如果要递归操作的话, 可以使用接下来要介绍的锁 NSRecursiveLock

接下来的几个锁就直接看看使用方法和存在的问题吧, 因为确实没有专门去研究实现什么的

2. NSRecursiveLock

1. 使用

 // 把上面的 NSLock 换成 NSRecursiveLock
 - (void)test2 {
     NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         static void (^block)(int);
         
         block = ^(int value) {
             NSLog(@"加锁 --------");
             [lock lock];
             NSLog(@"加锁成功 ----");
             if (value > 0) {
                 NSLog(@"value = %d", value);
                 block(value - 1);
             }
             NSLog(@"解锁 --------");
             [lock unlock];
         };
         block(10);
     });
 }
 // 结果
     运行正常

把上面 NSLock 出现问题的情况换成 NSRecursiveLock 后就没有问题了, NSRecursiveLock 允许在同一个线程中重复加锁。NSRecursiveLock 会记录加锁和解锁的次数, 当两者平衡的时候才会释放掉锁, 才允许其他线程上锁成功。

2. 存在的问题

那么如果在多个线程情况下呢, NSRecursiveLock 还管用吗?

 // 多条线程下
 - (void)test3 {
     NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
     for (int i = 0; i < 10; i++) {
         dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
             static void (^block)(int);
             
             block = ^(int value) {
                 NSLog(@"加锁 --------");
                 [lock lock];
                 NSLog(@"加锁成功 ----");
                 if (value > 0) {
                     NSLog(@"value = %d", value);
                     block(value - 1);
                 }
                 NSLog(@"解锁 --------");
                 [lock unlock];
             };
             block(10);
         });
     }
 }
 // 执行结果
 // 程序崩溃, 出现野指针错误
 // -[__NSCFType lock]: unrecognized selector sent to instance 0x600000dc1d40

原因分析:

线程 1 在执行中对对象进行了多次加锁, 然后线程 2 在执行中又对对象进行了多次加锁, 最终导致了两个线程直接相互等待对方解锁, 就导致了 死锁问题

如何解决呢?

可以使用之前分析过的 @synthronized 来进行解决, 因为它会进行缓存查找。如果该对象已经存在缓存中, 它是对他的加锁数量进行的变更, 并不会产生新的锁。

3. NSCondition

条件锁平时使用的不是很多, 与信号量相似, 就是当线程执行到锁只有需要满足一定的条件才能持有锁, 否则就会阻塞等待直到条件满足。

 - (void)testCondition1 {
     NSCondition *lock = [[NSCondition alloc] init];
     NSMutableArray *array = [NSMutableArray array];
     //
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         [lock lock];
         if (!array.count) {
             [lock wait];
         }
         [array removeAllObjects];
         NSLog(@"array removeAllObjects");
         [lock unlock];
     });
     //
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         sleep(1);
         [lock lock];
         [array addObject:@1];
         NSLog(@"array add 1");
         [lock signal];
         NSLog(@"发送信号");
         [lock unlock];
     });
 }
  1. NSCondition 可以对多个线程同时进行加锁操作, 当在加锁线程中调用 wait 方法后, 可以是线程进入休眠状态等待唤起的信号
  2. 在其他线程中通过调用 signal 或者 broadcast 方法可以唤醒已经休眠的线程, 不同点在于 signal 只能唤起一个休眠的线程, 而 broadcast 则可以唤醒所有休眠的线程。

4. NSConditionLock

 @interface NSConditionLock : NSObject <NSLocking> {
 @private
     void *_priv;
 }
 ​
 - (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
 ​
 @property (readonly) NSInteger condition;
 - (void)lockWhenCondition:(NSInteger)condition;
 - (BOOL)tryLock;
 - (BOOL)tryLockWhenCondition:(NSInteger)condition;
 - (void)unlockWithCondition:(NSInteger)condition;
 - (BOOL)lockBeforeDate:(NSDate *)limit;
 - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
 ​
 @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
 ​
 @end

NSConditionLockNSLock 的使用方式比较类似, 只是在获取锁的时候增加了一个条件限制, 然后多了一些针对条件 condition 的操作方法。下面来看看它的使用方法:

 //
 - (void)testConditonLock{
     // 信号量
     NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        [conditionLock lockWhenCondition:1];
        NSLog(@"线程 1");
        [conditionLock unlockWithCondition:0];
     });
     
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        
        [conditionLock lockWhenCondition:2];
        
        NSLog(@"线程 2");
        
        [conditionLock unlockWithCondition:1];
     });
     dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        [conditionLock lock];
        NSLog(@"线程 3");
        [conditionLock unlock];
     });
 }
 //  结果
 // 线程 2 或者 3 首先执行
 // 线程 1 的执行一定会在 2 之后
 // 如果先执行的 2, 3 和 1 的先后要看顺序    

下面就主要看看里面的加锁方法吧:

  1. lockNSLock 一样, 不需要判断条件, 如果可以的话就直接获取锁
  2. lockWhenCondition: 如果没有其他线程获取锁, 并且条件满足就能够获取锁, 如果条件不满足, 需要等待直到条件满足后才能获取锁
  3. unlockWithCondition: 释放锁, 同时把条件 condition 设置到想要的条件

condition 是一个整数值

5. 各种互斥锁的性能对比图

下面来放上一张各个所之间性能的对比图:

关于 dispatch_semapore 和 alloc 会放到后面的文章专门研究一下

image-20220612224117974

可以看到效率方面 OSSpinLock 的效率是最高的, 虽然它现在已经被弃用了。然而经常使用的 @synchronize 的效率是最差的。