iOS 锁的原理分析(一)

529 阅读4分钟

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

各种锁的性能分析

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);
    }

image.png

锁的性能分析表

在这里我们通过代码对 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 的底层代码实现。

image.png

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

image.png

我们运行下符号断点,可以看到是在 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 函数。

image.png

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

    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) {
                        
                    }
                }
            }
        });

image.png

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

image.png

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

id2data 函数执行流程

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

  1. id2data 函数第一次执行

image.png image.png

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

image.png image.png

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

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

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

@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 真机比模拟器性能高的原因

image.png

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