美文网首页
锁的原理

锁的原理

作者: 深圳_你要的昵称 | 来源:发表于2020-11-13 00:40 被阅读0次

    前言

    之前我们分析过多线程,知道了线程之间存在资源竞争的问题,为了解决这个问题,系统推荐了各种,保证当前只有一条线程对资源进行修改,从而保证数据的安全。今天外面重点看看系统提供了哪些,以及它们的底层源码,具体做了哪些流程?

    锁的性能

    在 ibireme 的 不再安全的 OSSpinLock 一文中,有一张图片简单的比较了各种锁的加解锁性能:

    上图可以看到除了OSSpinLock 外,dispatch_semaphorepthread_mutex 性能是最高的。但是OSSpinLock可能存在优先级反转的问题,那什么是优先级反转呢?

    首先我们看看正常情况下的线程调度原则👇

    系统将线程分为5个不同的优先级: backgroundutilitydefaultuser-initiateduser-interactive高优先级线程始终会在低优先级线程执行,一个线程不会受到比它更低优先级线程干扰

    但是可能存在这种现象👇(优先级反转)

    如果一个低优先级的线程获得并访问共享资源,这时一个高优先级的线程也尝试获得这个,它会处于 spin lock忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock,这就会破坏了spin lock,造成优先级反转。

    因此,苹果工程师不建议使用OSSpinLock自旋锁,而是尽量使用pthread_mutex替换。

    一、@synchronized

    @synchronized是我们最为熟悉的互斥锁,我们先来看看其底层实现流程是怎么样的。
    示例👇

    @synchronized (self) {
        NSLog(@"123");
     }
    

    1.1 寻找入口

    打断点,bt查看调用栈信息👇

    然并卵,再进入汇编(Always Show Disassembly)👇

    发现了两个objc_sync_enter objc_sync_exit,如果还不信,那么直接clang生成cpp验证👇

    xcrun -sdk iphonesimulator clang -rewrite-objc filename

    然后就是找objc_sync_enterobjc_sync_exit是在哪个库?还是去到汇编中👇


    注意:连续step into 2次

    找到了入口,原来在库libobjc.A.dylib中。

    1.2 objc_sync_enter & objc_sync_exit

    先看objc_sync_nil()源码👇

    BREAKPOINT_FUNCTION(
        void objc_sync_nil(void)
    );
    
    #   define BREAKPOINT_FUNCTION(prototype)                             \
        OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
        prototype { asm(""); }
    

    就是asm(""),确实没做任何处理!

    至此,我们发现,objc_sync_enterobjc_sync_exit都调用了id2data函数,这个是重点!

    1.3 id2data

    在查看id2data的源码之前,我们先看看几个重要的数据结构:

    • StripedMap
    • SyncList
    • SyncData

    为何将SyncData设计成单向链表这种结构?链表的优势在于插入、删除元素比普通的顺序表快,试想一下这个场景,单个线程里可支持加多个锁,多个线程加多个锁,加锁就好比是插入元素,如果采用顺序表,从头部开始查找,找到你想插入的位置再插入,就比较耗时了,所以采用链表的结构。

    以上的分析,可以得出下面这张图👇

    • 最左侧就是StripedMap,可以将它理解为一个哈希表,里面的元素是SyncList
    • SyncList是个结构体,其中包含成员SyncData
    • SyncData里又指向下一个SyncData,使得SyncList形成了一个单向链表的结构
    • 最终,StripedMap里的元素就是一个个的单向链表

    再回头来看id2data

    以上都是寻找object关联的SyncData对象,没找到就new一个,如果找到呢,做了什么流程?那么再看看👇

    1. 线程栈存中找到的处理
    1. 缓存中找到的处理

    首先看看fetch_cache流程👇

    接着来到_objc_fetch_pthread_data

    找到的pthread数据是个结构体👇

    里面的SyncCache👇

    再来看看SyncCache里找到SyncData后的处理流程👇

    1.4 done代码块的处理流程👇

    至此,id2data的流程分析完毕,大致就是:

    1. 查找锁对象object对应的SyncData对象,并返回。
    2. 如果线程栈存缓存object对应的pthread_data里的cacheItem缓存,以及全局静态单向链表里都没找到的话,那么就new一个SyncData对象
    3. 线程栈存缓存找到,就同步一下pthread_data里的cacheItem缓存,反之,也同步一下线程栈存缓存,保证两个缓存数据的同步一致

    但同时,要考虑场景,以enter为例:

    1. 第一次进来时
    1. 非第一次,同一个线程时
      如果支持线程栈存👇

    不支持线程栈存👇

    1. 非第一次,不同线程时

    然后去到done代码块,进行缓存处理

    最后回头来看@synchronized的底层源码,就清晰多了,找锁对象object对应的SyncData对象,调用该SyncData对象的成员recursive_mutex_t互斥锁完成加锁,并且加锁支持可重入(lockCount++)多个线程嵌套加(threadCount++)`。

    然后看解锁,其实与加锁查找流程一模一样的,只是最后处理的是lockCount--threadCount--👇

    二、NSLock & NSRecursiveLock

    2.1 NSLock

    使用方式很简单,lock unlock即可👇

        NSLock *lock = [[NSLock alloc] init];
        [lock lock];
        NSLog(@"123");
        [lock unlock];
    

    接着我们看看底层的实现👇

    老规矩,还是找入口。

    1. 打断点,看汇编,尝试step into👇


    step into根本进不去,放弃!

    1. lldb 查看bt👇


    还是没找到有用的信息,放弃!

    1. 下符号断点lock👇

    发现是Foundation,但是是闭源的。依旧放弃!

    1. 最终,还有一个渠道,就是看swift版本的源码,因为是开源的👇

    2.2 NSRecursiveLock

    我们先看一个嵌套block的案例

    - (void)lg_testRecursive{
        NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
        for (int i= 0; i<100; i++) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                static void (^testMethod)(int);
                testMethod = ^(int value){
                    if (value > 0) {
                        NSLog(@"current value = %d",value);
                        testMethod(value - 1);
                    }
                };
                testMethod(10);
            });
        }
    }
    

    运行👇

    数据会有重复,说明数据不安全!此时必须加锁,保证线程写数据的安全,
    尝试用NSLock👇

    直接阻塞了,根本没起到任何作用,因为这是一个递归,同时又是异步并发,是个多线程递归的调用,那么就会出现场景:线程之间会出现相互等待的情况。具体来说就是,线程1上锁后读取value值,还没解锁,此时线程2又进来加锁读取value值,线程2的任务作为了线程1的一个子任务,于是线程1的完成依赖线程2执行完成,但是线程2要执行完,必须等线程1解锁。

    应对上述的场景,系统提供了一个递归锁NSRecursiveLock,专门应对这种递归情况,使用如下👇

    接下来,我们看看递归锁的底层源码(还是swift开源版本)👇

    与NSLock源码对比,发现lockunlock方法是一样的,但仔细看初始化,是有些区别的👇

    对当前互斥锁,做了一个PTHREAD_MUTEX_RECURSIVE 类型的设置,这是个递归的类型,而NSLock却没有,说明是默认的类型。

    但是递归锁的使用,不是很好,在哪里lock,又在哪里unlock,很容易出错。👇

    这就是对锁的使用不熟练导致的,加锁解锁对象 --> 重点在于你要执行的任务,在执行任务前加锁,任务执行完成后解锁。

    如果不熟练,不如使用@syncronize,一个代码块搞定,根据我们底层的分析,它更试用于多线程的场景,lockCount++保证锁可重入,threadCount++保证多线程递归👇

    三、NSCondition & NSConditionLock

    3.1 NSCondition条件变量

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

    • 主要为了当检测条件时保护数据源,执行条件引发的任务;
    • 线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。

    对于NSCondition条件变量,有一个经典案例:生产消费者模型
    声明 + 初始化👇

    @interface ViewController ()
    @property (nonatomic, assign) NSUInteger ticketCount;
    @property (nonatomic, strong) NSCondition *testCondition;
    @end
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        self.ticketCount = 0;
        [self lg_testConditon];
    }
    

    生产者👇

    - (void)lg_producer{
         [_testCondition lock];
        self.ticketCount = self.ticketCount + 1;
        NSLog(@"生产一个 现有 count %zd",self.ticketCount);
        [_testCondition signal];
        [_testCondition unlock];
    }
    

    消费者

    - (void)lg_consumer{
        // 线程安全
         [_testCondition lock];
        
        if (self.ticketCount == 0) {
            NSLog(@"等待 count %zd",self.ticketCount);
            // 保证正常流程
             [_testCondition wait];
        }
        
        //注意消费行为,要在等待条件判断之后
        self.ticketCount -= 1;
        NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
        [_testCondition unlock];
    }
    

    调用👇

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

    run👇

    问题来了,感觉使用条件变量有些麻烦,除了lock unlock之外,还要一会signal,一会wait,根本不好控制,对于不熟练的开发者来说,很容易写错地方,导致crash。应对这样的情况,系统给我们提供了另一个锁NSConditionLock条件锁

    3.2 NSConditionLock条件锁

    相关概念
    • NSConditionLock条件锁,一旦一个线程获得锁,其他线程一定等待。
    • [xxxx lock];表示 xxx 期待获得锁,如果没有其他线程获得锁(不需要判断内部的 condition) 那它能执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件 锁),则等待,直至其他线程解锁。
    • [xxx lockWhenCondition:A条件];
      • 如果没有其他线程获得该锁,但是该锁内部的 condition不等于A条件,它依然不能获得锁,仍然等待
      • 如果内部的condition等于A条件,并且没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的完成,直至它解锁。
    • [xxx unlockWithCondition:A条件]; 表示释放锁,同时把内部的condition设置为A条件
    • return = [xxx lockWhenCondition:A条件 beforeDate:A时间]; 表示如果被锁定(没获得锁),并超过该时间不再阻塞线程。但是注意:返回的值是NO,它没有改变锁的状态,这个函数的目的在于可以实现两种状态下的处理
    • 所谓的condition就是整数,内部通过整数比较条件。
    案例分析
    - (void)lg_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];
        });
    }
    

    run👇

    分析:

    • 线程1 调用[NSConditionLock lockWhenCondition:],因为不满足当前条件,所
      以会进入 waiting 状态,会释放当前的互斥锁。线程2同理。
    • 此时当前的线程3 调用[NSConditionLock lock:],本质上是调用[NSConditionLock lockBeforeDate:],这里不需要比对条件值,所以线程3被优先打印
    • 接下来线程2 执行[NSConditionLock lockWhenCondition:],此时满足条件值,所以线程 2被打印,打印完成后会调用[NSConditionLock unlockWithCondition:],这个时候将
      value 设置为 1,并发送 boradcast,此时线程1接收到当前的信号,唤醒执行并打印。

    所以,当前打印为 线程 3 -->线程 2 --> 线程 1

    条件锁底层源码

    下面,我们依旧看看swift版的NSConditionLock的源码👇

    接着看看[NSConditionLock lockBeforeDate:]源码👇

    最后看看unlockWithCondition:源码👇

    总结

    开篇我们举出了日常使用的各种锁的性能对比,然后重点分析了@synchronize的底层原理,然后通过多线程递归示例的演示,接着分析了NSLock和递归锁NSRecursiveLock的底层,最后通过经典的生产消费者模型示例,分析了条件变量NSCondition和 条件锁NSConditionLock的底层源码处理流程。

    补充:读写锁

    读写锁适合于对数据结构的读次数比写次数多得多的情况。 因为读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁

    #include <pthread.h>
    // 成功则返回0, 出错则返回错误编号
    int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)
    

    同互斥量以上, 在释放读写锁占用的内存之前, 需要先通过pthread_rwlock_destroy对读写锁进行清理工作, 释放由init分配的资源。

    // 获取读锁
    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
    // 获取写锁
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
    // 释放锁
    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
    

    注意:获取锁的两个函数是阻塞操作。

    当然,那有没有非阻塞的函数👇

    int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
    

    非阻塞的获取锁操作, 如果可以获取则返回0, 否则返回错误的EBUSY.

    读写锁示例

    请参考XFReadWriteLocker,不对的地方请不吝赐教!

    相关文章

      网友评论

          本文标题:锁的原理

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