锁的原理

前言

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

锁的性能

在 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 
// 成功则返回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,不对的地方请不吝赐教!

你可能感兴趣的:(锁的原理)