0
点赞
收藏
分享

微信扫一扫

iOS 锁的原理分析(二)

编程练习生J 2021-09-19 阅读 65

锁的分类

自旋锁

互斥锁

这里属于互斥锁的有:

  • NSLock
  • pthread_mutex
  • @synchronized

条件锁

  • NSCondition
  • NSConditionLock

递归锁

  • NSRecursiveLock
  • pthread_mutex(recursive)

信号量

  • dispatch_semaphore

读写锁

总结

pthread

Posix Thread 中定义有一套专⻔用于线程同步的函数 mutex,用于保证在任何时刻,都只能有一个线程访问该对象。 当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。

  1. 创建和销毁
    A: POSIX 定义了一个宏 PTHREAD_MUTEX_INITIALIZER 来静态初始化互斥锁
    B: int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
    C: pthread_mutex_destroy () 用于注销一个互斥锁

  2. 锁操作

  • int pthread_mutex_lock(pthread_mutex_t *mutex)
  • int pthread_mutex_unlock(pthread_mutex_t *mutex)
  • int pthread_mutex_trylock(pthread_mutex_t *mutex)
  • pthread_mutex_trylock() 语义与 pthread_mutex_lock() 类似,不同的是在锁已经被占据时返回 EBUSY 而不是挂起等待。

NSLock 和 NSReLock 的分析

这里我们通过几个使用案例来介绍一下 NSLockNSReLock 这两种锁。

  • 案例 1



类似这样一段代码,当我们不加锁的情况下打印就会乱序,当我们在 testMethod(10) 执行前后分别加锁解锁就会循环按顺序打印。

  • 案例 2


类似这种,我们把 lockunlock 调整了下位置,就会出现类似死锁的现象,testMethod 递归执行。导致这个的原因是因为 NSLock 不具有可递归性。针对这种情况我们可以用 @synchronized 来解决,也可以用 NSRecursiveLock 来解决。因为在前面已经分析了 @synchronized,这里我们来试一下用 NSRecursiveLock 来解决,NSRecursiveLock 的使用频率也很高,我们在很多三方库里面在一些递归加锁的场景也可以看到 NSRecursiveLock 的应用。

  • 案例 3


当我们使用 NSRecursiveLock 的时候发现第一次可以打印,但是第二次就报错了,这是因为 NSRecursiveLock 具有可递归性,但是不支持多线程执行。

  • 案例 4

我们使用 @synchronized 既解决了递归调用,也解决了多线程的问题。

NSCondtion的分析

NSConditionapi介绍:

  • [condition lock]:一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在 lock 外等待,只到 unlock,才可访问。
  • [condition unlock]:与lock 同时使用。
  • [condition wait]:让当前线程处于等待状态。
  • [condition signal]:CPU发信号告诉线程不用在等待,可以继续执行。

案例

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

- (void)cx_producer {
    [_testCondition lock]; // 操作的多线程影响
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    if (self.ticketCount > 0) {
        [_testCondition signal]; // 信号
    }
    [_testCondition unlock];
}

- (void)cx_consumer {
     [_testCondition lock];  // 操作的多线程影响
    if (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        [_testCondition wait];
    }
    //注意消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
     [_testCondition unlock];
}

类似这样一段代码,我们定义了生产方法 cx_producer 跟消费方法 cx_consumer,在 ticketCount 值修改的时候都会加锁,但是在消费方法里面会判断 ticketCount 小于零的时候就会进入等待,禁止消费,在生产方法 cx_producer 中判断 ticketCount 大于零的时候就会发送信号,继续执行。保证了事务的安全。

foundation 源码关于锁的封装

我们前面也讲了,例如 NSLock, NSRecursiveLock, NSCondition 等这些锁的底层都是基于 pthread 的封装,但是这些锁的底层都是在 NSFoundation 框架下实现的,但是 NSFoundation 框架并不开源,我们怎么来探究它们的底层实现呢?这里我们取了个巧,用 swiftfoundation 框架源码来进行探究。源码已上传到 github,感兴趣的小伙伴可以下载。

NSLock

在我们的代码下我们我们按住 control + command 键点击进入 NSLock 的头文件实现可以看到 NSLock 是继承于 NSObject 的一个类,只是遵循了 NSLocking 协议。因为这里只能看到协议的声明,具体实现我们打开源码来看一下。


NSRecursiveLock

上面案例分析的时候我们知道 NSRecursiveLock 相对于 NSLock 具有可递归性,对比他们的源码我们可以看到,只是因为 NSRecursiveLock 的底层 pthread_mutex_init 的时候多了一个 attrs 参数。它们的 lockunlock 方法的底层实现都是一样。

NSCondition

查看 NSCondition 的源码实现,我们发现 NSCondition 只是在初始化的时候多了一句 pthread_cond_init(cond, nil),它的 wait 方法底层调用的是 pthread_cond_wait(cond, mutex)。通过对这几种锁的分析我们可以看到它们的底层都是基于 pthread 的封装,当我们不知道使用哪种锁的时候,用 pthread 来实现是最完美的。

NSConditionLock

NSConditionLock 介绍

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

案例

- (void)cx_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];
        sleep(0.1);
        NSLog(@"线程 2");
        [conditionLock unlockWithCondition:1];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       [conditionLock lock];
       NSLog(@"线程 3");
       [conditionLock unlock];
    });
}
  • 线程 1 调用 [NSConditionLock lockWhenCondition:],此时此刻因为不满足当前条件,所以会进入 waiting 状态,当前进入到 waiting 时,会释放当前的互斥锁。

  • 此时当前的线程 3 调用 [NSConditionLock lock:],本质上是调用 [NSConditionLock lockBeforeDate:],这里不需要比对条件值,所以线程 3 会打印

  • 接下来线程 2 执行 [NSConditionLock lockWhenCondition:],因为满足条件值,所以线程 2 会打印,打印完成后会调用[NSConditionLock unlockWithCondition:],这个时候将 value 设置为 1,并发送 boradcast, 此时线程 1 接收到当前的信号,唤醒执行并打印。

  • 自此当前打印为 线程 3->线程 2 -> 线程 1。

  • [NSConditionLock lockWhenCondition:]:这里会根据传入的 condition 值和 Value 值进行对比,如果不相等,这里就会阻塞,进入线程池,否则的话就继续代码执行。

  • [NSConditionLock unlockWithCondition:]:这里会先更改当前的 value 值,然后进行广播,唤醒当前的线程。

NSConditionLock 执行流程分析

通过上面的案例我们会有几个疑问:

  • NSConditionLockNSCondition 有什么区别
  • 初始化的时候 [[NSConditionLock alloc] initWithCondition:2] 会传入一个参数 2,这个值的作用是干什么的
  • lockWhenCondition 是如何控制流程的
  • unlockWithCondition 又做了什么

前面几种锁我们都是通过源码看到了底层的实现,但是当我们没有源码的时候我们又应该用哪种思路去分析呢?这里我们尝试一下通过反汇编跟来探索一下。这里环境用的是真机。

  • initWithCondition 流程追踪

首先对 initWithCondition 方法下符号断点 -[NSConditionLock initWithCondition:],这里需要注意因为是对象方法,所以符号断点有点特殊。

断点之后我们可以看到汇编代码,这里 x0, x1, x2 分别代表方法的调用者, 调用方法, 参数。这里我们输出之后跟我们 OC 代码的调用都能一一对应上。这里我们重点追踪 bl 的执行,因为 bl 代表跳转。下面我们就一步一步的断点 bl 的执行。

这里 x0 输出暂时看不到,但是可以看到调用了 init 方法,并且参数是 2。

这里追踪到 NSConditionLock 调用了 init 方法,并且参数是 2。

这里 NSConditionLock 调用了 zone 方法,也就是内存开辟。

这里 NSCondition 调用了 allocWithZone 方法。

这里 NSCondition 调用了 init 方法。

这里就是 returnx0 就是返回对象,打印 x0 的内存结构,可以看到它有 NSCondition 跟 2 两个成员变量。

  • lockWhenCondition 流程追踪


这里 NSDate 调用了 distantFuture 方法且参数为 1。

这里执行了 waitUntilDate 方法,进行了等待。

这里 NSConditionLock 调用了 lockWhenCondition:beforeDate:,第一个参数为 1,第二个参数为 [NSDate distantFuture] 的返回值。并且在这里新增符号断点 -[NSConditionLock lockWhenCondition:beforeDate:]

这里会断到 lockWhenCondition:beforeDate: 方法。


lockWhenCondition:beforeDate: 之后会再次来到 lockWhenCondition 方法,只是到了线程 4,参数变为了 2。

线程 4 中 lockWhenCondition 之后还会来到 lockWhenCondition:beforeDate: 方法。在 bl 这里 NSCondition 调用了 lock 方法。

  • unlockWithCondition 流程追踪


这里会来到 unlockWithCondition 方法,并且也进行了加锁操作。

这里 NSCondition 调用了 broadcast 方法。

方法结束后 NSCondition 调用了 unlock 方法。

紧接着这里会来到我们 OC 代码中的线程一中的 lockWhenCondition:beforeDate: 方法,在这里又进行了一次解锁操作,跟上面我们两次加锁一一对应上了。

执行结束也返回了 0x0000000000000001,也就是 1。

最后执行 OC 代码中的线程一中的 unlockWithCondition 方法。然后又会执行上面 unlockWithCondition 方法的汇编流程。这里 1 代表不在等待。

反汇编分析与源码对比


通过对比我们可以看到我们反汇编分析的执行流程与源码逻辑一致。

GCD???????????????? 实现多读单写

????????????????????????????????????????????????????????????????????????????????比如在内存中维护一份数据,有多处地方可能同时操作这块数据,怎么能保证数据库的安全呢?这里需要满足以下三点:

  • 1.读写互斥
  • 2.写写互斥
  • 3.读读并发
- (instancetype)init {
    self = [super init];
    if (self) {
        _currentQueue = dispatch_queue_create("chenxi", DISPATCH_QUEUE_CONCURRENT);
        _dic = [NSMutableDictionary dictionary];
    }
    return self;
}

- (void)cx_setSafeObject:(id)object forKey:(NSString *)key {
    key = [key copy];
    __weak __typeof(self)weakSelf = self;
    dispatch_barrier_async(_currentQueue, ^{
        [weakSelf.dic setObject:object forKey:key];
    });
}

- (id)cx_safeObjectForKey:(NSString *)key {
    __block id temp;
    __weak __typeof(self)weakSelf = self;
    dispatch_sync(_currentQueue, ^{
        temp = [weakSelf.dic objectForKey:key];
    });
    return temp;
}
  • 首先我们要维系一个GCD队列,最好不用全局队列,毕竟大家都知道全局队列遇到栅栏函数是有坑点的,这里就不分析了!

  • 因为考虑性能, 死锁, 堵塞的因素不考虑串行队列,用的是自定义的并发队列!

  • 首先我们来看看读操作: cx_safe0bjectForKey 我们考虑到多线程影响是不能用异步函数的!说明:

    • 线程 2 获取: name 线程 3 获取 age
    • 如果因为异步并发,导致混乱本来读的是 name 结果读到了 age
    • 我们允许多个任务同时进去! 但是读操作需要同步返回,所以我们选择:同步函数(读读并发)
  • 我们再来看看写操作,在写操作的时候对 key 进行了 copy,关于此处的解释,插入一段来自参考文献的引用:

  • 这里我们选择 dispatch_barrierasync,为什么是栅栏函数而不是异步函数或者同步函数,下面分析:

    • 栅栏函数任务:之前所有的任务执行完毕,并且在它后面的任务开始之前,期间不会有其他的任务执行,这样比较好的促使写操作一个接一个写(写写互斥),不会乱!
    • 为什么不是异步函数? 应该很容易分析,毕竟会产生混乱!
    • 为什么不用同步函数?如果读写都操作了,那么用同步函数,就有可能存在:我写需要等待读操作回来才能执行,显然这里是不合理!
举报

相关推荐

0 条评论