美文网首页底层原理
iOS 锁的原理分析(一)

iOS 锁的原理分析(一)

作者: 晨曦的简书 | 来源:发表于2021-08-22 21:37 被阅读0次

    在我们的日常开发中肯定都有过锁的使用,那么这些锁的底层原理是如何实现的呢?各种锁的性能区别又有多大呢?在这一篇章我们来探究一下。

    各种锁的性能分析

    int cx_runTimes = 100000;
        /** OSSpinLock 性能 */
        {
            OSSpinLock cx_spinlock = OS_SPINLOCK_INIT;
            double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
            for (int i=0 ; i < cx_runTimes; i++) {
                OSSpinLockLock(&cx_spinlock);          //解锁
                OSSpinLockUnlock(&cx_spinlock);
            }
            double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
            CXLog(@"OSSpinLock: %f ms",(cx_endTime - cx_beginTime)*1000);
        }
        
        /** dispatch_semaphore_t 性能 */
        {
            dispatch_semaphore_t cx_sem = dispatch_semaphore_create(1);
            double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
            for (int i=0 ; i < cx_runTimes; i++) {
                dispatch_semaphore_wait(cx_sem, DISPATCH_TIME_FOREVER);
                dispatch_semaphore_signal(cx_sem);
            }
            double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
            CXLog(@"dispatch_semaphore_t: %f ms",(cx_endTime - cx_beginTime)*1000);
        }
        
        /** os_unfair_lock_lock 性能 */
        {
            os_unfair_lock cx_unfairlock = OS_UNFAIR_LOCK_INIT;
            double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
            for (int i=0 ; i < cx_runTimes; i++) {
                os_unfair_lock_lock(&cx_unfairlock);
                os_unfair_lock_unlock(&cx_unfairlock);
            }
            double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
            CXLog(@"os_unfair_lock_lock: %f ms",(cx_endTime - cx_beginTime)*1000);
        }
        
        
        /** pthread_mutex_t 性能 */
        {
            pthread_mutex_t cx_metext = PTHREAD_MUTEX_INITIALIZER;
          
            double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
            for (int i=0 ; i < cx_runTimes; i++) {
                pthread_mutex_lock(&cx_metext);
                pthread_mutex_unlock(&cx_metext);
            }
            double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
            CXLog(@"pthread_mutex_t: %f ms",(cx_endTime - cx_beginTime)*1000);
        }
        
        
        /** NSlock 性能 */
        {
            NSLock *cx_lock = [NSLock new];
            double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
            for (int i=0 ; i < cx_runTimes; i++) {
                [cx_lock lock];
                [cx_lock unlock];
            }
            double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
            CXLog(@"NSlock: %f ms",(cx_endTime - cx_beginTime)*1000);
        }
        
        /** NSCondition 性能 */
        {
            NSCondition *cx_condition = [NSCondition new];
            double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
            for (int i=0 ; i < cx_runTimes; i++) {
                [cx_condition lock];
                [cx_condition unlock];
            }
            double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
            CXLog(@"NSCondition: %f ms",(cx_endTime - cx_beginTime)*1000);
        }
    
        /** PTHREAD_MUTEX_RECURSIVE 性能 */
        {
            pthread_mutex_t cx_metext_recurive;
            pthread_mutexattr_t attr;
            pthread_mutexattr_init (&attr);
            pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
            pthread_mutex_init (&cx_metext_recurive, &attr);
            
            double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
            for (int i=0 ; i < cx_runTimes; i++) {
                pthread_mutex_lock(&cx_metext_recurive);
                pthread_mutex_unlock(&cx_metext_recurive);
            }
            double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
            CXLog(@"PTHREAD_MUTEX_RECURSIVE: %f ms",(cx_endTime - cx_beginTime)*1000);
        }
        
        /** NSRecursiveLock 性能 */
        {
            NSRecursiveLock *cx_recursiveLock = [NSRecursiveLock new];
            double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
            for (int i=0 ; i < cx_runTimes; i++) {
                [cx_recursiveLock lock];
                [cx_recursiveLock unlock];
            }
            double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
            CXLog(@"NSRecursiveLock: %f ms",(cx_endTime - cx_beginTime)*1000);
        }
        
    
        /** NSConditionLock 性能 */
        {
            NSConditionLock *cx_conditionLock = [NSConditionLock new];
            double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
            for (int i=0 ; i < cx_runTimes; i++) {
                [cx_conditionLock lock];
                [cx_conditionLock unlock];
            }
            double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
            CXLog(@"NSConditionLock: %f ms",(cx_endTime - cx_beginTime)*1000);
        }
    
        /** @synchronized 性能 */
        {
            double_t cx_beginTime = CFAbsoluteTimeGetCurrent();
            for (int i=0 ; i < cx_runTimes; i++) {
                @synchronized(self) {}
            }
            double_t cx_endTime = CFAbsoluteTimeGetCurrent() ;
            CXLog(@"@synchronized: %f ms",(cx_endTime - cx_beginTime)*1000);
        }
    
    锁的性能分析表

    在这里我们通过代码对 10 种锁进行了测试,并制作了表格,这里是在 iphone12 真机环境下进行的,这里我们可以发现一个问题,在我们的印象中 @synchronized 是比较消耗性能的,但是这里的测试的好像还好。这是因为开发过程中 @synchronized 的使用频率比较高,苹果在 arm64 下对 @synchronized 做了性能优化,这里后面我们会进行分析。这 10 种锁里面因为 dispatch_semaphore_t 在讲 GCD 的时候已经分析过了,这里就不在讲了。pthread_mutex_tpthread_mutex_t(recurive) 因为调用的是 pthreadapi,这里也不再讲了。其实我们每种锁的最底层都是基于 pthread 实现的,如果想验证某种锁的性能,跟 pthread 来做比较就好。

    @synchronized 分析

    @synchronized 原理分析上

    因为我们平时开发过程中 @synchronized 使用频率最高,这里我们就来先探索一下 @synchronized 的原理。

    int main(int argc, char * argv[]) {
        NSString * appDelegateClassName;
        @autoreleasepool {
            appDelegateClassName = NSStringFromClass([AppDelegate class]);
            @synchronized (appDelegateClassName) {
            }
        }
        return UIApplicationMain(argc, argv, nil, appDelegateClassName);
    }
    

    类似这段代码,我们通过生成 cpp 文件来看一下 @synchronized 的底层代码实现。

    通过底层代码我们可以看到,如果加锁成功我们需要看的就是 objc_sync_enter(_sync_obj)objc_sync_exit(_sync_obj) 这两段代码。

    我们运行下符号断点,可以看到是在 libobjc.A.dylib 库调的 objc_sync_enter 函数,所以我们下载 libobjc.A.dylib 源码具体来分析一下。

    objc_sync_enterobjc_sync_exit 源码探究

    int objc_sync_enter(id obj)
    {
        int result = OBJC_SYNC_SUCCESS;
    
        if (obj) {
            SyncData* data = id2data(obj, ACQUIRE);
            ASSERT(data);
            data->mutex.lock();
        } else {
            // @synchronized(nil) does nothing
            if (DebugNilSync) {
                _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
            }
            objc_sync_nil();
        }
    
        return result;
    }
    
    int objc_sync_exit(id obj)
    {
        int result = OBJC_SYNC_SUCCESS;
        if (obj) {
            SyncData* data = id2data(obj, RELEASE); 
            if (!data) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            } else {
                bool okay = data->mutex.tryUnlock();
                if (!okay) {
                    result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
                }
            }
        } else {
            // @synchronized(nil) does nothing
        }
        return result;
    }
    

    通过源码可以看到,objc_sync_enterobjc_sync_exit 函数刚开始都会先判断 obj,如果 obj 为空,通过注释也可以看到,相当于什么都不做,然后通过 id2data 函数获取 SyncData ,只是 objc_sync_enterobjc_sync_exit 函数传的参数不一样,且 objc_sync_enter 函数会调用 data->mutex.lock() 加锁, objc_sync_exit 函数会调用 data->mutex.tryLock() 解锁。

    • SyncData 数据结构
    typedef struct alignas(CacheLineSize) SyncData {
        struct SyncData* nextData; // 类似链表结构,下一个节点
        DisguisedPtr<objc_object> object; // 对 object 包装成 DisguisedPtr 结构
        int32_t threadCount;  // 代表线程数量
        recursive_mutex_t mutex; // 通过 pthread 定义了一个递归锁 mutex
    } SyncData;
    

    id2data 函数分析

    通过上面对 objc_sync_enterobjc_sync_exit 函数的分析,可以看到他们都调用了 id2data 函数,这里我们来重点分析下 id2data 函数。

    因为这个函数内的代码比较多,我们先整体分析下这个函数大致做了哪些事情。

        spinlock_t *lockp = &LOCK_FOR_OBJ(object);
        SyncData **listp = &LIST_FOR_OBJ(object);
        SyncData* result = NULL;
    

    这里我们来先看看这个函数最开始的时候通过 &LOCK_FOR_OBJ(object) 获取到 lockp,通过 & LIST_FOR_OBJ(object) 获取到 listp,这里我们看看这两个宏定义。

    #define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
    #define LIST_FOR_OBJ(obj) sDataLists[obj].data
    static StripedMap<SyncList> sDataLists;
    

    这里可以看到,这两个宏定义其实都是对 sDataLists 方法的定义。这里我们也可以看到 sDataLists 是一个全局的哈希表,表里面存储的是 SyncList 结构类型的数据。

    • SyncList
    struct SyncList {
        SyncData *data;
        spinlock_t lock;
    
        constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
    };
    
    • sDataLists

    这里我们通过 lldb 来查看一下 sDataLists 的数据结构。

    CXPerson *p1 = [[CXPerson alloc] init];
            CXPerson *p2 = [[CXPerson alloc] init];
            CXPerson *p3 = [[CXPerson alloc] init];
            dispatch_async(dispatch_queue_create("cx", DISPATCH_QUEUE_CONCURRENT), ^{
                @synchronized (p1) {
                    @synchronized (p2) {
                        @synchronized (p3) {
                            
                        }
                    }
                }
            });
    

    通过打印我们可以看到 StripedMap 里面存储的每个元素是 SyncListSyncListdataSyncData 数据结构的链表。

    这个 StripedMap 是一张全局的哈希表,每个象对应一个 SyncList,同一个对象每加锁一次会对 data 链表插入一个 SyncData,虽然都是一个对象,但是 SyncData 不同,当对对象解锁的时候就会删除对应的 SyncData

    id2data 函数执行流程

    这里我们详细的来分析一下 id2data 函数的执行流程。

    1. id2data 函数第一次执行

    1. id2data 函数第二次执行 (@synchronized 参数不是同一个对象)

    1. @synchronized 加锁同一个对象,且不是第一次

    这里 OSAtomicDecrement32Barrier 函数会对 threadCount 减 1,threadCount 代表同一个对象在不同线程进行加锁,线程的数量。

    1. @synchronized 加锁同一个对象,且不是第一次并且不在同一个线程

    @synchronized 总结

    • 1: @synchronized 会有一张全局哈希表 sDataLists,数据存储采用的是拉链法
    • 2: sDataLists 是一个 array,存储的是 SyncListSyncListobjc 对应。
    • 3: objc_sync_enter 函数跟 objc_sync_ exit 函数成对出现,底层是基于 pthread 封装的递归锁
    • 4: 支持两种存储 : tls / cache
    • 5: 第一次调用id2data 函数,会创建一个 syncData 并进行头插法,生成一个链表,并标记 thracount = 1
    • 6: 判断是不是同一个对象进来
    • 7: TLS -> lockCount ++
    • 8: TLS 找不到上一个 SyncData,会重新创建一个 SyncData,并对 threadCount ++
    • 9: lockCouture--, threadCount--

    @synchronized 支持递归并支持多线程的原因:

      1. TLS 保障了可以用 threadCount 来标记有多少条线程对这个锁对象进行加锁。
      1. lockCount 用来标记在当前线程空间锁对象被加锁了多少次。

    补充

    • TLS 线程相关解释

    线程局部存储 (Thread Local Storage,TLS): 是操作系统为线程单独提供的私有空间,通常只有有限的容量。Linux 系统下通常通过 pthread库中的 pthread_key_create()pthread_getspecific()pthread_setspecific()pthread_key_delete()

    • @synchronized 使用注意事项
    • @synchronized 参数不要为空。
    • 要注意 @synchronized 加锁的对象的生命周期
    • @synchronized 加锁对象为同一个对象时方便数据的存储与释放(这里有一个问题就是会导致 SyncList 链表过长,会对内存操作行成负担,但是一般不会出现这种情况)。

    • @synchronized 真机比模拟器性能高的原因

    通过源码可以看到真机 StripeCount 为 8,模拟器 StripeCount 为 64。StripeCount 越大数据存储的就会越大,数据操作的时候需要查询的数据也会越多,这是导致真机比模拟器性能高的原因。

    相关文章

      网友评论

        本文标题:iOS 锁的原理分析(一)

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