美文网首页iOS 进阶之路iOS-日常总结
OC底层原理二十九:NSLock、NSCondition、NSC

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

作者: markhetao | 来源:发表于2020-11-11 17:29 被阅读0次

    OC底层原理 学习大纲

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

    • 锁家族全家福(耗时图):
      image.png
    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
    
    • 进入NSLock,可以看到它遵循NSLocking协议:
    @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 lock]加锁处中,加入断点,打开debug汇编模式,一步步执行,查询源码的出处: 很遗憾,发现找不到

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

      image.png
    • 可是Foudation库是未开源库,我们无法获取源码。但是swift开源语言。我们可以参考swift Foudation库

    • 打开swift Foundation库,搜索class NSLock:

      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三者的区别

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

    • 案例:循环生成多个全局队列的异步线程,每个线程内声明block(testMethod)-> 实现block -> 调用block -> 嵌套调用block(递归调用)

    • 要求: 分别使用NSLockNSRecursiveLock@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锁,必须在声明前加锁调用后解锁,才能解决数据读写安全问题。

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

    2.2 使用NSRecursiveLock

    • 声明前加锁调用后解锁是正确的。

      image.png
    • 但由于它具备递归特性,我们在block内部递归前当前线程也打印正常,但是其他线程堵塞

      image.png
    • 当我们去掉for循环,仅保持一个异步线程,在block内部递归前后分别加锁解锁,打印正常:

      image.png

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

    image.png

    2.3 @synchronized

    • @synchronized最简单,直接将block内部代码包裹起来,就可以实现数据读写安全

      image.png
    • 关于@synchronized的内部结构,我们上一节专门分析了。

    • @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发信号告诉所有线程不用再等待,可以继续执行
    • 测试案例:
      2个生产者2个消费者各自生产和消费各50次。当消费者购买时,没货排队等待有货卖货,一次只能一个人买, 每当生产者生产一个货物时,都会广播告诉所有等待消费者,进行继续购买
      这样保障货品数据安全(有货才能卖,一次卖一个,没货就等待)
    @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
    
    • 打印结果:(生产消费数据是安全的)


      image.png

    但是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];
        });
    }
    
    • 打印结果:


      image.png

    分析:

    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广播给所有等待的线程
    • Swift Foundation源码中搜索NSConditionLock,可以看到循环检查线程、条件上锁过程:
      image.png

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

    (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是否相等。才进行后续操作

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

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

    相关文章

      网友评论

        本文标题:OC底层原理二十九:NSLock、NSCondition、NSC

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