iOS 底层探索:常见的锁

作者: 欧德尔丶胡 | 来源:发表于2020-11-17 18:09 被阅读0次

    iOS 底层探索: 学习大纲 OC篇

    前言

    • 上一篇讲了@Synchronized 这个互斥递归锁的底层的原理,今天来拓展一下iOS中其他锁的一些原理,然后进行对比分析。

    一、 常见锁的性能对比

    可以看出,图中锁的性能从高到底依次是:
    OSSpinLock(自旋锁) :性能最高
    synchronized(互斥锁):性能最低

    二、 锁的分类

    互斥锁

    它将代码切片成为一个个代码块,使得当一个代码块在运行时,其他线程不能运行他们之中的任意片段,只有等到该片段结束运行后才可以运行。通过这种方式来防止多个线程同时对某一资源进行读写的一种机制。常用的有:

    • @synchronized
    • NSLock
    • pthread_mutex

    自旋锁

    多线程同步的一种机制,当其检测到资源不可用时,会保持一种“忙等”的状态,直到获取该资源。它的优势在于避免了上下文的切换,非常适合于堵塞时间很短的场合;缺点则是在“忙等”的状态下会不停的检测状态,会占用 cpu 资源。常用的有:

    • OSSpinLock
    • atomic

    条件锁

    通过一些条件来控制资源的访问,当然条件是会发生变化的。常用的有:

    • NSCondition
    • NSConditionLock

    信号量

    是一种高级的同步机制。互斥锁可以认为是信号量取值0/1时的特例,可以实现更加复杂的同步。常用的有:

    • dispatch_semaphore

    递归锁

    它允许同一线程多次加锁,而不会造成死锁。递归锁是特殊的互斥锁,主要是用在循环或递归操作中。常用的有:

    • pthread_mutex(recursive)
    • NSRecursiveLock

    读写锁

    是并发控制的一种同步机制,也称“共享-互斥锁”,也是一种特殊的自旋锁。它把对资源的访问者分为读者和写者,它允许同时有多个读者访问资源,但是只允许有一个写者来访问资源。常用的有:

    • pthread(rwlock)
    • dispatch_barrier_async / dispatch_barrier_sync

    三、常见几种锁的使用方法及底层原理

    3.1、atomic(原子锁)

    atomic适用于OC中属性的修饰符,其自带一把自旋锁,但是这个一般基本不使用,都是使用的nonatomic

    我们知道setter方法会根据修饰符调用不同方法,其中最后会统一调用reallySetProperty方法,其中就有atomicnonatomic操作

    static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
    {
       ...
       id *slot = (id*) ((char*)self + offset);
       ...
    
        if (!atomic) {//未加锁
            oldValue = *slot;
            *slot = newValue;
        } else {//加锁
            spinlock_t& slotlock = PropertyLocks[slot];
            slotlock.lock();
            oldValue = *slot;
            *slot = newValue;        
            slotlock.unlock();
        }
        ...
    }
    

    从源码中可以看出,对于atomic修饰的属性,进行了spinlock_t加锁处理,但是OSSpinLock已经废弃了,这里的spinlock_t在底层是通过os_unfair_lock替代了OSSpinLock实现的加锁。同时为了防止哈希冲突,还是用了加盐操作

    using spinlock_t = mutex_tt<LOCKDEBUG>;
    
    class mutex_tt : nocopy_t {
        os_unfair_lock mLock;
        ...
    }
    

    getter方法中对atomic的处理,同setter是大致相同的

    id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
        if (offset == 0) {
            return object_getClass(self);
        }
    
        // Retain release world
        id *slot = (id*) ((char*)self + offset);
        if (!atomic) return *slot;
            
        // Atomic retain release world
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();//加锁
        id value = objc_retain(*slot);
        slotlock.unlock();//解锁
        
        // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
        return objc_autoreleaseReturnValue(value);
    }
    

    3.2、synchronized(互斥递归锁)

    已分析过,请查看iOS 底层探索:Dispatch_source & @Synchronized

    3.3、NSLock

    NSLock是对下层pthread_mutex的封装,使用如下

     NSLock *lock = [[NSLock alloc] init];
    [lock lock];
    [lock unlock];
    
    

    直接进入NSLock定义查看,其遵循了NSLocking协议,下面来探索NSLock的底层实现

    3.3.1 NSLock 底层分析

    • NSLock源码在Foundation框架中, 由于OC的Foundation框架不开源,所以这里参考Swift的开源框架Foundation来 分析NSLock的底层实现,其原理与OC是大致相同的
    image.png
    • 通过源码实现可以看出,底层是通过pthread_mutex互斥锁实现的。并且在init方法中,还做了一些其他操作,所以在使用NSLock时需要使用init初始化

    回到前文的性能图中,可以看出NSLock的性能仅次于 pthread_mutex(互斥锁),非常接近

    3.3.2 使用弊端

    请问下面block嵌套block的代码中,会有什么问题?

    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);
        });
    }  
    
    
    • 在未加锁之前,其中的current=9、10有很多条,导致数据混乱,主要原因是多线程导致的

    • 如果像下面这样加锁,会有什么问题?

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

    其运行结果如下

    会出现一直等待的情况,主要是因为嵌套使用的递归,使用NSLock(简单的互斥锁,如果没有回来,会一直睡觉等待),即会存在一直加lock,等不到unlock 的堵塞情况

    所以,针对这种情况,可以使用以下方式解决

    • 使用@synchronized
    for (int i= 0; i<100; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void (^testMethod)(int);
            testMethod = ^(int value){
                @synchronized (self) {
                    if (value > 0) {
                      NSLog(@"current value = %d",value);
                      testMethod(value - 1);
                    }
                }
            };
            testMethod(10); 
        });
    }
    
    • 使用递归锁NSRecursiveLock
    NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
     for (int i= 0; i<100; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void (^testMethod)(int);
            [recursiveLock lock];
            testMethod = ^(int value){
                if (value > 0) {
                  NSLog(@"current value = %d",value);
                  testMethod(value - 1);
                }
                [recursiveLock unlock];
            };
            testMethod(10);
        });
    }
    

    3.4 pthread_mutex

    pthread_mutex就是互斥锁本身,当锁被占用,其他线程申请锁时,不会一直忙等待,而是阻塞线程并睡眠。

    使用

    // 导入头文件
    #import <pthread.h>
    
    // 全局声明互斥锁
    pthread_mutex_t _lock;
    
    // 初始化互斥锁
    pthread_mutex_init(&_lock, NULL);
    
    // 加锁
    pthread_mutex_lock(&_lock);
    // 这里做需要线程安全操作
    // 解锁 
    pthread_mutex_unlock(&_lock);
    
    // 释放锁
    pthread_mutex_destroy(&_lock);
    

    3.5、NSRecursiveLock

    • 声明前加锁调用后解锁是正确的。

      image
    • 但由于它具备递归特性,我们在block内部递归前当前线程也打印正常,但是其他线程堵塞

      image
    • 当我们去掉for循环,仅保持一个异步线程,在block内部递归前后分别加锁解锁,打印正常:

      image

    这是因为NSRecursiveLock递归特性。内部任务是递归持有的,所以不会死锁

    image

    3.6、NSCondition

    NSCondition 是一个条件锁,在日常开发中使用较少,与信号量有点相似:线程1需要满足条件1才会往下走,否则会堵塞等待,知道条件满足。经典模型是生产消费者模型

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

    • 主要 为了当检测条件时保护数据源,执行条件引发的任务

    • 线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞

    使用

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

    通过swift的Foundation源码查看NSCondition的底层实现

    open class NSCondition: NSObject, NSLocking {
        internal var mutex = _MutexPointer.allocate(capacity: 1)
        internal var cond = _ConditionVariablePointer.allocate(capacity: 1)
        //初始化
        public override init() {
            pthread_mutex_init(mutex, nil)
            pthread_cond_init(cond, nil)
        }
        //析构
        deinit {
            pthread_mutex_destroy(mutex)
            pthread_cond_destroy(cond)
    
            mutex.deinitialize(count: 1)
            cond.deinitialize(count: 1)
            mutex.deallocate()
            cond.deallocate()
        }
        //加锁
        open func lock() {
            pthread_mutex_lock(mutex)
        }
        //解锁
        open func unlock() {
            pthread_mutex_unlock(mutex)
        }
        //等待
        open func wait() {
            pthread_cond_wait(cond, mutex)
        }
        //等待
        open func wait(until limit: Date) -> Bool {
            guard var timeout = timeSpecFrom(date: limit) else {
                return false
            }
            return pthread_cond_timedwait(cond, mutex, &timeout) == 0
        }
        //信号,表示等待的可以执行了
        open func signal() {
            pthread_cond_signal(cond)
        }
        //广播
        open func broadcast() {
            // 汇编分析 - 猜 (多看多玩)
            pthread_cond_broadcast(cond) // wait  signal
        }
        open var name: String?
    }
    
    

    其底层也是对下层pthread_mutex的封装

    • NSCondition是对mutexcond的一种封装(cond就是用于访问和操作特定类型数据的指针)

    • wait操作会阻塞线程,使其进入休眠状态,直至超时

    • signal操作是唤醒一个正在休眠等待的线程

    • broadcast会唤醒所有正在等待的线程

    3.6、NSConditionLock

    NSConditionLock是条件锁,一旦一个线程获得锁,其他线程一定等待

    相比NSConditionLock而言,NSCondition使用比较麻烦,所以推荐使用NSConditionLock,其使用如下

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

    NSConditionLock,其本质就是NSCondition + Lock,以下是其swift的底层实现,

    open class NSConditionLock : NSObject, NSLocking {
        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
        }
    
        open func lock() {
            let _ = lock(before: Date.distantFuture)
        }
    
        open func unlock() {
            _cond.lock()
            _thread = nil
            _cond.broadcast()
            _cond.unlock()
        }
    
        open var condition: Int {
            return _value
        }
    
        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)
        }
    
        open func unlock(withCondition condition: Int) {
            _cond.lock()
            _thread = nil
            _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
                }
            }
             _thread = pthread_self()
            _cond.unlock()
            return true
        }
    
        open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
            _cond.lock()
            while _thread != nil || _value != condition {
                if !_cond.wait(until: limit) {
                    _cond.unlock()
                    return false
                }
            }
            _thread = pthread_self()
            _cond.unlock()
            return true
        }
    
        open var name: String?
    }
    
    

    通过源码可以看出

    • NSConditionLockNSCondition的封装

    • NSConditionLock可以设置锁条件,即condition值,而NSCondition只是信号的通知

    四、性能总结

    • OSSpinLock自旋锁由于安全性问题,在iOS10之后已经被废弃,其底层的实现用os_unfair_lock替代

      • 使用OSSpinLock及所示,会处于忙等待状态

      • os_unfair_lock是处于休眠状态

    • atomic原子锁自带一把自旋锁,只能保证setter、getter时的线程安全,在日常开发中使用更多的还是nonatomic修饰属性

      • atomic:当属性在调用setter、getter方法时,会加上自旋锁osspinlock,用于保证同一时刻只能有一个线程调用属性的读或写,避免了属性读写不同步的问题。由于是底层编译器自动生成的互斥锁代码,会导致效率相对较低

      • nonatomic:当属性在调用setter、getter方法时,不会加上自旋锁,即线程不安全。由于编译器不会自动生成互斥锁代码,可以提高效率

    • @synchronized在底层维护了一个哈希表进行线程data的存储,通过链表表示可重入(即嵌套)的特性,虽然性能较低,但由于简单好用,使用频率很高

    • NSLockNSRecursiveLock底层是对pthread_mutex的封装

    • NSConditionNSConditionLock是条件锁,底层都是对pthread_mutex的封装,当满足某一个条件时才能进行操作,和信号量dispatch_semaphore类似

    五、 锁的使用场景

    • 如果只是简单的使用,例如涉及线程安全,使用NSLock即可

    • 如果是循环嵌套,推荐使用@synchronized,主要是因为使用递归锁的性能不如使用@synchronized的性能(因为在synchronized中无论怎么重入,都没有关系,而NSRecursiveLock可能会出现崩溃现象)

    • 循环嵌套中,如果对递归锁掌握的很好,则建议使用递归锁,因为性能好

    • 如果是循环嵌套,并且还有多线程影响时,例如有等待、死锁现象时,建议使用@synchronized

    相关文章

      网友评论

        本文标题:iOS 底层探索:常见的锁

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