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

本章提纲:
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底层学习之八大锁的分析)