iOS 锁的原理分析(一)

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

各种锁的性能分析

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

%s 个评论

要回复文章请先登录注册