美文网首页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