美文网首页
27.iOS底层学习之八大锁的分析

27.iOS底层学习之八大锁的分析

作者: 牛牛大王奥利给 | 来源:发表于2022-01-19 15:54 被阅读0次

    本章提纲:
    1、NSLock
    2、NSRecursiveLock
    3、NSCondition
    4、NSConditionLock
    5、读写锁的实现

    上一篇我们了解了synchronized的使用,这篇文章来补充其他我们平时开发中常用的一些锁的示例,以及简单的源码探索。

    1.NSLock

    • NSLock的基本使用
    //NSLock的创建
     NSLock *nlock = [[NSLock alloc] init];
    
    //NSLock加锁
    [nlock lock];
    
    // NSLock解锁
     [nlock unlock];
    

    它的使用非常简单,下面我们来结合具体示例。

    1.1 NSLock的使用示例
    • 测试代码
      多线程递归打印,未加锁之前
      未加锁.png
      可以看到打印结果,在未经过任何临界区处理,没有加锁的情况下,打印的结果非常随意混乱。

    多线程递归打印,递归外部加锁NSLock

    递归外部加锁NSLock
    可以看到结果变得开始有顺序了,在一次线程操作未处理完毕之前,下一个线程不能访问,所以每次线程执行递归的,都是有序的打印。

    多线程递归打印,递归内部加锁NSLock

    递归内部加锁NSLock
    可以看到,只打印了10,程序就没有后续了,在递归内部,加了一次锁之后,testMethod又调用了自己,然后又走到[lock lock];因为这段已经加了锁,所以要等着解锁才能再一次执行testMethod,因为解锁的代码在testMethod之后,永远也走不到,所以这里有点死锁的状态。

    通过上面三种情况的讨论,可以了解到NSLock可以用于多线程使线程安全,起到保护临界区的作用。但是不能在递归中使用。我们后边要学习的NSRecursiveLock是可以在递归中使用的,后面详细介绍。

    1.2 NSLock源码窥探

    我们打一个符号断点-[NSLock lock],看看NSLock的底层实现所在的框架。

    NSLock符号断点

    可以看到NSLock是出自于Foundation框架,而oc版本的Foundation框架并没有开源,我们通过Swift版本的来看一下里面的具体实现,打开swift-corelibs-foundation-master代码,搜索NSLock,来到它的定义的部分。

    NSLock源码定义
    1、我们通过这个源码可以了解到NSLock遵循NSLocking,而NSLocking是协议。
    2、而NSLock的初始化init实际上调用的是pthread_mutex_initpthread_mutex_initpthread框架下的一个API,所以NSLock的实际实现其实是对pthread的封装。
    • pthread简介
      pthread是 POSIX threads 的简称,是POSIX的线程标准。POSIX是可移植操作系统接口(Portable Operating System Interface)的简称,其定义了操作系统的标准接口,旨在获得源代码级别的软件可移植性。它是一套通用的多线程API,适用于Unix、Linux、Windows等系统,跨平台、可移植,使用难度大,C语言框架,线程生命周期由程序员管理。

    从源码的部分点进去,可以看到一部分pthread的API,有初始化的,加锁,解锁的等等。


    pthread

    2. NSRecursiveLock

    同样NSRecursiveLock也是对pthread的封装,具体的实现和NSLock的差别我们在下面源码的时候说明。先来看它和NSLock在使用上的差别。还是上面的代码做示例。

    2.1 NSRecursiveLock的使用示例

    多线程递归打印,递归外部加锁NSRecursiveLock

    递归外部加锁NSRecursiveLock
    它的效果和NSLock是一样的。

    多线程递归打印,递归内部加锁NSRecursiveLock

    递归内部加锁NSRecursiveLock
    可以看到,一次的递归调用完成了,打印出了完整的10到1。我具体的打了个断点,看看这个递归锁在递归中是怎么执行的,
    具体调试
    左边线程堆栈展示的invoke2比十个多,但是我们递归一共调用了十次,也就是说别的线程有的调用了lock也记到了当前线程里边去,所以在解锁的时候当前线程多调用了unlock,所以引发了崩溃。

    通过这个表现我们可以看出来NSRecursiveLock支持递归使用,但是不支持多线程!

    2.2 NSRecursiveLock源码窥探

    我们来到源码看到NSRecursiveLock的实现和NSLock很相似。

    image.png
    多了个attr的初始化和设置,传的参数是PTHREAD_MUTEX_RECURSIVE
     pthread_mutexattr_init(attrs)
     pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
    

    而且在pthread_mutex_init()调用时,NSLock传的是nil,而NSRecursiveLock把初始化好的attrs传了进去。

    3. NSCondition

    NSCondition是条件锁,它的使用方式和信号量很相似,当线程符合条件的时候才会执行,否则会等待被阻塞,等待满足条件时再执行。

    • API展示
    //加锁
    [condition lock];
    
    //与lock同时使⽤,解锁
    [condition unlock];
    
    //使当前线程处于等待状态
    [condition wait];
    
    //CPU发信号告诉线程不⽤等待,继续执⾏
    [condition signal];
    
    3.1 NSCondition的使用示例
    3.1.1生产者消费者问题简介

    生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

    3.1.2解决这类问题的办法

    要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。
    (上述介绍摘自百度百科)

    • NSCondition简单使用案例
      这里我们简单实现了下生产者生产,然后消费者消费的一个简单案例,只有生产者通知消费者可以消费的这么一个简单的实现。
     (void)testConditon{
        
        _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];
    }
    
    • 消费者来到方法consumer,发现如果共享区域的资源self.ticketCount如果是0了,那么要等待,[_testCondition wait];。如果不为0,那么进行"消费",资源数-1。
    • 然后生产者调用方法producer,对共享资源self.ticketCount进行+1的操作,然后发送signal信号,通知正在等待消费的消费者,生产好了一个,可以唤醒一个等待消费的"消费者"。
    • 而上述操作公共资源self.ticketCount的时候都需要加锁,防止多线程访问。
    3.2 NSCondition源码窥探
    NSCondition(1)
    NSCondition(2)

    它也是对pthread的封装。

    • init方法调用了pthread_mutex_initpthread_cond_init
    • lock调用了pthread_mutex_lock,unlock调用pthread_mutex_unlock
    • wait调用pthread_cond_wait.
    • signal调用的是pthread_cond_signal
    • broadcast调用的是pthread_cond_broadcast

    4. NSConditionLock

    NSConditionLock也是条件锁,实际上是对NSCondition的再封装。使用更加灵活。

    • 相关API
          //无条件锁 和NSLock没什么区别 底层都是pthread_mutex_lock 和pthread_mutex_unlock
            [conditionLock lock];
            [conditionLock unlock];
      
          //有条件锁 当condition满足条件时加锁,不满足条件不执行
          - (void)lockWhenCondition:(NSInteger)condition;
    
        //有条件,解锁时 condition设置成相应的条件,符合这个条件的其他等待的线程就能执行了。
          - (void)unlockWithCondition:(NSInteger)condition;
    
    4.1 NSConditionLock使用示例
    NSConditionLock示例

    1、NSConditionLock的对象初始化条件为2,三个异步的线程,只有condition为2的条件里能执行。
    2、线程1执行条件是1,所以不符合条件,不往下执行。线程2满足条件,可以执行;线程3没有条件限制,也可以执行。
    3、因为都是异步而且在并发队列中,线程2优先级是DISPATCH_QUEUE_PRIORITY_LOW,线程3是defualt默认优先级,而且线程2还睡眠了0.1,所以3大概率在2的前边执行。
    4、而线程1要等着线程2执行完把condition置为1才能执行。

    所以执行的顺序是3,2,1。

    4.2 NSConditionLock源码窥探分析

    回到源码,直接搜索NSConditionLock,实现如下。

    open class NSConditionLock : NSObject, NSLocking {
        //1、_cond是NSCondition()的对象,_value是Int类型。
        //方法open var condition返回的就是_value。所以这个_value实际上就是初始化condition的值
        internal var _cond = NSCondition()
        internal var _value: Int
        internal var _thread: _swift_CFThreadRef?
        
        public convenience override init() {
            self.init(condition: 0)
        }
        
        public init(condition: Int) {
            _value = condition
        }
    
        //2、lock真实调用的是 open func lock(before limit: Date) -> Bool,传入的参数是固定的 .distantFuture
        open func lock() {
            let _ = lock(before: Date.distantFuture)
        }
    
        //3、解锁时 调用的是broadcast也就是pthread_cond_broadcast,和unlock。
        open func unlock() {
            _cond.lock()
    #if os(Windows)
            _thread = INVALID_HANDLE_VALUE
    #else
            _thread = nil
    #endif
            _cond.broadcast()
            _cond.unlock()
        }
        
        //返回的是value
        open var condition: Int {
            return _value
        }
    
        //4、lock(whenCondition condition: Int) 底层实际调用的是lock(whenCondition condition: Int, before limit: Date) -> Bool
        //limit传入的是固定的.distantFuture
        open func lock(whenCondition condition: Int) {
            let _ = lock(whenCondition: condition, before: Date.distantFuture)
        }
    
        open func `try`() -> Bool {
            return lock(before: Date.distantPast)
        }
        
        open func tryLock(whenCondition condition: Int) -> Bool {
            return lock(whenCondition: condition, before: Date.distantPast)
        }
    
        
        //5、unlock(withCondition condition: Int)调用的是broadcast和unlock
        open func unlock(withCondition condition: Int) {
            _cond.lock()
    #if os(Windows)
            _thread = INVALID_HANDLE_VALUE
    #else
            _thread = nil
    #endif
            _value = condition
            _cond.broadcast()
            _cond.unlock()
        }
    
        open func lock(before limit: Date) -> Bool {
            _cond.lock()
            while _thread != nil {
                if !_cond.wait(until: limit) {
                    _cond.unlock()
                    return false
                }
            }
    #if os(Windows)
            _thread = GetCurrentThread()
    #else
            _thread = pthread_self()
    #endif
            _cond.unlock()
            return true
        }
        
        open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
            _cond.lock()
            //线程不为空 或者 传进来的 _value和传进来的condition不相同进入循环
            while _thread != nil || _value != condition {
                //解锁条件 具体看_cond.wait的实现,
                if !_cond.wait(until: limit) {
                    _cond.unlock()
                    return false
                }
            }
    #if os(Windows)
            _thread = GetCurrentThread()
    #else
            _thread = pthread_self()
    #endif
            _cond.unlock()
            return true
        }
        
        open var name: String?
    }
    

    1、_cond是NSCondition()的对象,_value是Int类型。所以这个_value实际上就是初始化condition的值。

    2、lock真实调用的是 open func lock(before limit: Date) -> Bool,传入的参数是固定的 .distantFuture。

    3、解锁时 调用的是broadcast也就是pthread_cond_broadcast,和unlock。

    4、lock(whenCondition condition: Int) 底层实际调用的是lock(whenCondition condition: Int, before limit: Date) -> Bool,limit传入的是固定的.distantFuture。

    5、unlock(withCondition condition: Int)调用的是broadcast和unlock

    • wait(until limit: Date) -> Bool
     open func wait(until limit: Date) -> Bool {
    #if os(Windows)
            return SleepConditionVariableSRW(cond, mutex, timeoutFrom(date: limit), 0)
    #else
            //timeOut不成立 就走else 也就是timeout为nil(timeout为nil时是date传的值不大于0) 就走else 否则跳过语句
            guard var timeout = timeSpecFrom(date: limit) else {
                return false
            }
            
            //接着等待
            return pthread_cond_timedwait(cond, mutex, &timeout) == 0
    #endif
        }
    
    
    • timeSpecFrom
        public static let distantFuture = Date(timeIntervalSinceReferenceDate: 63113904000.0)
    
       public var timeIntervalSinceNow: TimeInterval {
            return self.timeIntervalSinceReferenceDate - CFAbsoluteTimeGetCurrent()
        }
    
    private func timeSpecFrom(date: Date) -> timespec? {
        //date.timeIntervalSinceNow 不大于0 返回nil ,大于0 跳过这个语句。
        guard date.timeIntervalSinceNow > 0 else {
            return nil
        }
        let nsecPerSec: Int64 = 1_000_000_000
        let interval = date.timeIntervalSince1970
        let intervalNS = Int64(interval * Double(nsecPerSec))
    
        return timespec(tv_sec: Int(intervalNS / nsecPerSec),
                        tv_nsec: Int(intervalNS % nsecPerSec))
    }
    

    结合上述代码,可以了解到wait方法,当传进来的limit再次是distantFuture时,timeSpecFrom才走guard语句中的返回,才不继续等待。传进来的不是distantFuture返回的是当前的时间,也就是大于0的值,是ture

    distantFuture 从代码定义看,是一个非常大的值。

    (而这个终止等待的条件,有可能是在broadcast广播的时候,或者在unlock触发跳出while的条件,后面尝试验证一下)。

    5.读写锁的实现

    • 读写锁简介
      读写锁,又叫共享-独占锁。从命名上来看,读写锁拥有两把锁,读锁写锁。它的特点是:
    • 同一时间只允许一个线程对共享资源进行写操作;
    • 当进行写操作的时候,同一时间其他线程都会被阻塞;
    • 当进行读操作的时候,同一时间所有的写操作都会被阻塞;
    • 当进行读操作的时候,同一时间其他线程可以进行读操作共享资源;
    具体实现

    根据上述要求,我们可以总结出来,就是写的时候,在写之前的所有操作要完毕,只能进行一个写操作。这符合GCD栅栏函数的特性,栅栏里的内容执行之前,会阻塞后续的同一队列上的任务,栅栏之前的操作执行完,才会执行栅栏里的操作。所以写放到栅栏里,然后所有的任务并发就ok了。

    - (void)testReaderAndWriter{
        self.testQueue = dispatch_queue_create("abc", DISPATCH_QUEUE_CONCURRENT);
        for (int i = 0; i < 50; i++) {
            [self writer];
            [self reader];
            [self reader];
            [self writer];
            [self reader];
        }
    }
    
    //读者
    - (void)reader{
        dispatch_async(self.testQueue, ^{
            NSLog(@"读取剩余票数%lu",(unsigned long)self.ticketCount);
        });
    }
    
    //写操作
    - (void)writer{
        //进行写操作
        dispatch_barrier_sync(self.testQueue, ^{
            self.ticketCount++;
        });
    }
    

    实现结果

    实现结果

    相关文章

      网友评论

          本文标题:27.iOS底层学习之八大锁的分析

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