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

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,527评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,314评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,535评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,006评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,961评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,220评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,664评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,351评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,481评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,397评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,443评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,123评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,713评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,801评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,010评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,494评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,075评论 2 341

推荐阅读更多精彩内容

  • 目录:1.为什么要线程安全2.多线程安全隐患分析3.多线程安全隐患的解决方案4.锁的分类-13种锁4.1.1OSS...
    二斤寂寞阅读 1,170评论 0 3
  • 概念 自旋锁: 线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种忙等待。一旦获取了自旋锁,线...
    MonKey_Money阅读 840评论 2 1
  • 了解锁的机制会有助于项目开发,从而避免项目中多个线程访问同一块资源引发数据混乱的问题。 一 概念 锁的归类 基本...
    yan0_0阅读 282评论 0 2
  • 回顾之前 前文讲到多线程原理,线程安全、线程阻塞、线程使用等;这节我们来分析一下有关线程安全的一部分:锁,线程锁。...
    孜孜不倦_闲阅读 873评论 0 2
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,458评论 16 22