iOS 进阶之路iOS-日常总结

OC底层原理二十九:NSLock、NSCondition、NSC

2020-11-11  本文已影响0人  markhetao

OC底层原理 学习大纲

上一节锁家族@synchronized进行源码解析,本节将对锁家族的其他2位NSLockNSCondition进行源码分析。

  1. NSLock应用与源码
  2. NSLock、NSRecursiveLock、@synchronized三者的区别
  3. NSCondition
  4. NSConditionLock

1. NSLock

@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *testArray;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self demo];
}

- (void)demo {
    NSLog(@"123");
    self.testArray = [NSMutableArray array];
    NSLock * lock = [[NSLock alloc] init]; // 创建
    for (int i = 0; i < 20000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock]; // 加锁
            self.testArray = [NSMutableArray array];
            [lock unlock]; // 解锁
        });
    }
}
@end
@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

@interface NSLock : NSObject <NSLocking> { ... }
 ... 
@end

@interface NSConditionLock : NSObject <NSLocking>   { ... }
 ... 
@end

@interface NSRecursiveLock : NSObject <NSLocking>  { ... }
 ... 
@end

@interface NSCondition : NSObject <NSLocking>  { ... }
 ... 
@end
  • NSLocking协议包含lockunlock两个方法。
  • NSLockNSConditionLockNSRecursiveLockNSCondition都遵循NSLocking协议
  • 方法一: 在代码[lock lock]加锁处中,加入断点,打开debug汇编模式,一步步执行,查询源码的出处: 很遗憾,发现找不到

  • 方法二: 直接断点进不去,那我们运行到断点处后,加入lock符号断点,再运行代码,发现找到了,在Foundation库中执行的:

    image.png

我们发现:

  • 1.init中初始化了pthread_mutex
    1. lockunlock实际都是调用pthread_mutex相对于的lockunlock函数

顺便探究NSRecursiveLockNSConditionNSConditionLock

  • 发现NSRecursiveLockNSCodition也是基于pthread_mutex封装的,但:
  • NSRecursiveLockNSLock多了一层递归逻辑
  • NSCoditionNSLock多了一层pthread_con_init条件锁。
  • NSConditionLock是在NSCondition的基础上进行的再次封装。
NSRecursiveLock NSCondition
NSConditionLock

结论:

    1. 必须调用init方法(new内部也调用了init方法),因为init会完成底层pthread_mutex相关锁初始化
    1. 所有遵循NSLocking协议的类,底层都是基于pthread_mutex锁来实现的,只是封装深度不同
    1. NSLock性能接近pthread_mutex,而pthread_mutex(recursive)NSRecursiveLockNSConditionNSConditionLock耗时一个比一个,就是由对pthread_mutex封装深度决定的。

2. NSLock、NSRecursiveLock、@synchronized三者的区别

我们通过一个案例来进行分析对比

- (void)demo{
    for (int i= 0; i<10; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
           
            static void (^testMethod)(int);// 1. 声明
            
            testMethod = ^(int value){  // 2. 实现Block块
                if (value > 0) {
                  NSLog(@"current value = %d",value);
                  testMethod(value - 1); // 4. 嵌套调用block
                }
            };
            
            testMethod(10); // 3.调用block
        });
    }
}

2.1 使用NSLock:

必须在Block实现前加锁,在调用后解锁

image.png

相关实践:

  1. 调用前加锁: 死锁
    image.png
  • 仅在第一次进入block打印了一次,后面就死锁了。
    (一直lock加锁,而没有unlock解锁导致的)

2.调用后加锁: 无效锁

image.png
  • 打印结果完全无序的作用完全消失
    (想想都知道,block执行完了,你再上锁有啥用了一堆寂寞 😂 )

💣 NSLock锁,只锁了当前线程,当我们使用异步多线程操作时,可能出现线程相互等待死锁的情况

2.2 使用NSRecursiveLock

这是因为NSRecursiveLock递归特性。内部任务是递归持有的,所以不会死锁

image.png

2.3 @synchronized

NSLock、NSRecursiveLock、@synchronized三者的区别

  • NSLock:

    1. 需要手动创建释放,需要在准确的时机进行相应操作
    2. 仅锁住当前线程当前任务无法自动实现线程间通讯递归问题。
      (上述NSLock代码实际上没解决递归问题,只是野蛮的代码最外层上了一把大锁无视递归内部层级
  • NSRecursiveLock:

    1. 需要手动创建释放,需要在准确的时机进行相应操作
    2. 仅锁住当前线程所有任务无法自动实现线程间通讯,但可以解决递归问题。
      (与NSLock不同,NSRecursiveLock是在递归时,每层加锁解锁。对锁的控制更为精确
  • @synchronized:

    1. 只需将需要锁代码都放在作用域内,确定被锁对象(被锁对象决定了锁的生命周期),@synchronized就可以做到自动创建释放
    1. 被锁对象所有线程所有任务自动实现线程间通讯,可以解决递归问题
      (内部逻辑为: 被锁对象可持有多个线程每个线程递归持有多个任务)

所以我们日常使用时,尽管@synchronized耗时较大,但是它使用非常简单,根本不需要处理各种异常情况,也不需要手动释放便捷性安全性非常好

3. NSCondition

NSCondition的对象实际上是作为一个和一个线程检查器:

方法:

  • [condition lock]: 加锁
    (一般用于多线程同时访问修改同一个数据源时,保证同一时间内数据源只能被访问修改一次其他线程的命令需要在lock外等待,只有unlock后,才可访问
  • [condition unlock]: 解锁(与lock配对使用)
  • [condition wait]:当前线程处于等待状态
  • [condition signal]:CPU发信号告诉所有线程不用再等待,可以继续执行
@interface ViewController ()
@property (nonatomic, assign) NSUInteger ticketCount;
@property (nonatomic, strong) NSCondition *testCondition;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.ticketCount = 0;
    [self demo];
}

- (void)demo{
    
    _testCondition = [[NSCondition alloc] init];
    
    //创建生产-消费者
    for (int i = 0; i < 50; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self producer]; // 生产者
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self consumer]; // 消费者
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self consumer]; // 消费者
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self producer]; // 生产者
        });
    }
}

- (void)producer{
    [_testCondition lock]; // 操作的多线程影响
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    [_testCondition signal]; // 发送信号
    [_testCondition unlock];
}

- (void)consumer{
 
     [_testCondition lock];  // 操作的多线程影响
    if (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        [_testCondition wait]; // 线程等待
    }
    //注意消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
     [_testCondition unlock];
}
@end

但是NSCondition使用非常麻烦,需要在合适的地方手动加锁等待发送信号释放
于是基于NSCondition,出现了NSConditionLock

4. NSConditionLock

NSConditionLock是一把,一旦一个线程获得其他线程一定等待

方法:

  • [xxx lock]: 加锁

    • 如果没有其他线程获得(不需要判断内部的condition),那他能执行后续代码,同时设置当前线程获得
    • 如果已经其他线程获得(可能是条件锁,或者无条件锁),则等待直到其他线程解锁
  • [xxx lockWhenCondition: A条件]:

    • [xxx lock]基础上,没有其他线程获得,且内部condition条件满足A条件时,执行后续代码并让当前线程获得否则依旧是等待
  • [xxx unlockWithCondition: A条件]:释放

    • 内部的condition设置为A条件,并broadcast广播告诉所有等待的线程
  • return = [xxx lockWhenCondition: A条件 beforeDate: A时间]:

    • 没有其他线程获得,且满足A条件,且在A时间之前,可以执行后续代码并让当前线程获得
    • 返回值为NO,表示没有改变锁的状态
  • condition整数,内部通过整数比较条件

- (void)demo{

    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
         [conditionLock lockWhenCondition:1]; // conditoion = 1 内部 Condition 匹配
        NSLog(@"线程 1");
         [conditionLock unlockWithCondition:0]; // 解锁并把conditoion设置为0
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
       
        [conditionLock lockWhenCondition:2]; // conditoion = 2 内部 Condition 匹配
        sleep(0.1);
        NSLog(@"线程 2");
        [conditionLock unlockWithCondition:1]; // 解锁并把conditoion设置为1
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
       [conditionLock lock]; 
       NSLog(@"线程 3");
       [conditionLock unlock];
    });
}

分析:

  1. 有三个并行队列+异步函数,分别处理三个任务,三个任务的执行顺序无序
    并行队列+异步线程是的执行顺序是不固定的,取决于任务资源大小cpu的调度
  2. 我们init时,将condition设置为2。
    • 任务1: 必须当前线程没被锁,且condition1时,我才加锁执行后面代码
    • 任务2: 必须当前线程没被锁,且condition2时,我才加锁并执行后面代码
    • 任务3: 必须当前线程没被锁,我可以加锁并执行后面代码

所以任务3执行时期不确定,只要当前线程没被锁,随时都可以。 任务1一定在任务2后面

  • 因为condition初始值为2,只有任务2满足条件,任务2执行完后,将condition设置为1,并broadcast广播给所有等待的线程
  • 此时正在等待任务1的线程收到广播,检查任务1满足条件任务1执行完后,将condition设置为0,并broadcast广播给所有等待的线程

感兴趣的,我们可以汇编验证部分流程
(汇编机器执行代码,是最准确执行顺序找不到源码时,只有它才是最有效探索路径)

(PS: 汇编确实很难懂,这里只是简单介绍一下部分流程,主要是思路的拓宽)

  • 简化测试代码:
- (void)demo{
   
   NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
   
   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
       [conditionLock lockWhenCondition:2]; // conditoion = 2 内部 Condition 匹配
       NSLog(@"线程 2");
       [conditionLock unlockWithCondition:1]; // 解锁并把conditoion设置为1
   });
}
  • lockWhenCondition加上断点,打开汇编模式:
image.png
image.png
  • 运行代码,执行到断点处,再加入lockWhenCondition:符号断点:(注意:冒号不能少前后不能有空格),再运行代码

    image.png
  • lockWhenCondition:beforeDate:一行加断点运行至此处,读取参数,发现beforeDate的默认值是distantFuture

    image.png
  • 加入lockWhenCondition:beforeDate:符号断点,运行代码,进入到该函数内:

    image.png
  • 发现首先调了lock函数,我们加入lock断点,运行代码,发现内部是NSCondition执行了lock方法:

    image.png

回到上一页,我们在pthread_equal下一行加入断点,运行代码。打印相应值:

image.png
  • pthread_equal检查线程是否存在,true:跳到0x7fff207ef545false:比较r15rbx偏移0x10位。
    这里实际就是检查线程是否存在,如果不存在,再检查condition是否相等。才进行后续操作

... 大概思路就是这样... 讲个思路就行。 真正的汇编探索,还需要很大的基本功海量训练

关于锁的探索,到此为止。 其他类型的锁,可以用类似方式探索研究

上一篇下一篇

猜你喜欢

热点阅读