iOS :锁的性能分析& @synchronized原理分析

准备

objc4-818.2 源码

一、锁性能分析

常见锁性能分析示例代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    int kc_runTimes = 40000;
    /** OSSpinLock 性能 */
    {
        OSSpinLock kc_spinlock = OS_SPINLOCK_INIT;
        double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < kc_runTimes; i++) {
            OSSpinLockLock(&kc_spinlock);          //解锁
            OSSpinLockUnlock(&kc_spinlock);
        }
        double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
        KCLog(@"OSSpinLock: %f ms",(kc_endTime - kc_beginTime)*1000);
    }
    
    /** dispatch_semaphore_t 性能 */
    {
        dispatch_semaphore_t kc_sem = dispatch_semaphore_create(1);
        double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < kc_runTimes; i++) {
            dispatch_semaphore_wait(kc_sem, DISPATCH_TIME_FOREVER);
            dispatch_semaphore_signal(kc_sem);
        }
        double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
        KCLog(@"dispatch_semaphore_t: %f ms",(kc_endTime - kc_beginTime)*1000);
    }
    
    /** os_unfair_lock_lock 性能 */
    {
        os_unfair_lock kc_unfairlock = OS_UNFAIR_LOCK_INIT;
        double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < kc_runTimes; i++) {
            os_unfair_lock_lock(&kc_unfairlock);
            os_unfair_lock_unlock(&kc_unfairlock);
        }
        double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
        KCLog(@"os_unfair_lock_lock: %f ms",(kc_endTime - kc_beginTime)*1000);
    }
    
    /** pthread_mutex_t 性能 */
    {
        pthread_mutex_t kc_metext = PTHREAD_MUTEX_INITIALIZER;
      
        double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < kc_runTimes; i++) {
            pthread_mutex_lock(&kc_metext);
            pthread_mutex_unlock(&kc_metext);
        }
        double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
        KCLog(@"pthread_mutex_t: %f ms",(kc_endTime - kc_beginTime)*1000);
    }
    
    /** NSlock 性能 */
    {
        NSLock *kc_lock = [NSLock new];
        double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < kc_runTimes; i++) {
            [kc_lock lock];
            [kc_lock unlock];
        }
        double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
        KCLog(@"NSlock: %f ms",(kc_endTime - kc_beginTime)*1000);
    }
    
    /** NSCondition 性能 */
    {
        NSCondition *kc_condition = [NSCondition new];
        double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < kc_runTimes; i++) {
            [kc_condition lock];
            [kc_condition unlock];
        }
        double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
        KCLog(@"NSCondition: %f ms",(kc_endTime - kc_beginTime)*1000);
    }

    /** PTHREAD_MUTEX_RECURSIVE 性能 */
    {
        pthread_mutex_t kc_metext_recurive;
        pthread_mutexattr_t attr;
        pthread_mutexattr_init (&attr);
        pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
        pthread_mutex_init (&kc_metext_recurive, &attr);
        
        double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < kc_runTimes; i++) {
            pthread_mutex_lock(&kc_metext_recurive);
            pthread_mutex_unlock(&kc_metext_recurive);
        }
        double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
        KCLog(@"PTHREAD_MUTEX_RECURSIVE: %f ms",(kc_endTime - kc_beginTime)*1000);
    }
    
    /** NSRecursiveLock 性能 */
    {
        NSRecursiveLock *kc_recursiveLock = [NSRecursiveLock new];
        double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < kc_runTimes; i++) {
            [kc_recursiveLock lock];
            [kc_recursiveLock unlock];
        }
        double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
        KCLog(@"NSRecursiveLock: %f ms",(kc_endTime - kc_beginTime)*1000);
    }

    /** NSConditionLock 性能 */
    {
        NSConditionLock *kc_conditionLock = [NSConditionLock new];
        double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < kc_runTimes; i++) {
            [kc_conditionLock lock];
            [kc_conditionLock unlock];
        }
        double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
        KCLog(@"NSConditionLock: %f ms",(kc_endTime - kc_beginTime)*1000);
    }

    /** @synchronized 性能 */
    {
        double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < kc_runTimes; i++) {
            @synchronized(self) {}
        }
        double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
        KCLog(@"@synchronized: %f ms",(kc_endTime - kc_beginTime)*1000);
    }
}
复制代码

iphoneX真机执行结果:

OSSpinLock: 0.485063 ms
dispatch_semaphore_t: 0.717044 ms
os_unfair_lock_lock: 0.768065 ms
pthread_mutex_t: 0.838041 ms
NSlock: 1.092076 ms
NSCondition: 1.066923 ms
PTHREAD_MUTEX_RECURSIVE: 1.288056 ms
NSRecursiveLock: 1.695037 ms
NSConditionLock: 5.481005 ms
@synchronized: 3.665924 ms
复制代码
  • 可以看到@synchronized的执行速度并不是最慢的,说明系统进行了优化。

iphoneX模拟器执行结果:

OSSpinLock: 0.509977 ms
dispatch_semaphore_t: 0.768065 ms
os_unfair_lock_lock: 0.856042 ms
pthread_mutex_t: 1.099944 ms
NSlock: 1.405954 ms
NSCondition: 1.512051 ms
PTHREAD_MUTEX_RECURSIVE: 1.899004 ms
NSRecursiveLock: 3.090978 ms
NSConditionLock: 4.673004 ms
@synchronized: 5.235076 ms
复制代码
  • 可以看到在模拟器中@synchronized的效率低了不少,为什么呢,我们最下面会进行分析。

锁性能分析表:

image.png

  • dispatch_semaphore_t我们在 上一篇 分析过。
  • pthread_mutex_tPTHREAD_MUTEX_RECURSIVE,是底层api的直接调用也不做底层的分析。
  • @synchronized我们平时用的比较多,接下来将进行@synchronized的底层探索,其他锁在下一篇进行分析。

二、@synchronized 源码引出

我们想对@synchronized进行分析,先要知道它调用了什么方法,我们可以有两种方式进行探索:1. clang编译出C++代码,2. 汇编跟源码。

汇编跟源码 找方法

我们今天用汇编跟源码的方式进行分析,如下:

image.png

  • @synchronized调用了objc_sync_enterobjc_sync_exit两个函数,接下来对他们进行分析。

符号断点 找库

我们在工程中搜索objc_sync_enterobjc_sync_exit是搜索不到的,那么通过下符号断点的方式来查找它们:

image.png

  • 可以看到objc_sync_enter是在libobjc.A.dylib库中的,也就是我们平时分析的 objc4-818.2 源码,下面打开源码,进行底层源码的探索。

三、@synchronized 底层数据结构分析

objc_sync_enter & objc_sync_exit

源码进入objc_sync_enter

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;
}
复制代码
  • 如果obj存在,则通过id2data方法获取相应的SyncData,对threadCountlockCount进行递增操作。
  • 如果obj不存在,则调用objc_sync_nil,什么也没做。

进入objc_sync_exit

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;
}
复制代码
  • 如果obj存在,则调用id2data方法获取对应的SyncData,对threadCountlockCount进行递减操作。
  • 如果objnil,什么也不做。

通过上面两个实现逻辑的对比,发现它们有一个共同点,在obj存在时,都会通过id2data方法,获取SyncData类型的对象,我们先来探索一下SyncData的数据结构是怎么样的?

SyncData 的数据结构

查看SyncData的数据结构

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;
复制代码
  • SyncData是一个结构体,通过nextData可以知道这是一个单向链表,且封装了recursive_mutex_t属性,可以确认@synchronized是一个递归互斥锁。(recursive:递归的 ,mutex:互斥锁)

整体数据结构(sDataLists)

进入id2data函数:

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);// 这个锁是本函数内用的,一定要和外面的做区分
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
    
    ...先隐藏后面代码
}
复制代码

查看LOCK_FOR_OBJLIST_FOR_OBJ的定义:

image.png

  • sDataLists是一个全局哈希表
  • 表中元素是SyncList:
    image.png
  • 我们在 OC 类原理探索:cache 的结构 文章中分析过,当通过哈希函数获取下标时,如果发生哈希冲突,会通过再哈希的方式来寻找下标。
  • 而这里,则是通过另外一种方式来解决哈希冲突,叫做拉链法,下面会进行分析。

打印sDataLists查看数据结构:

image.png

  • 可以看到,sDataLists中有个array,是一个有这64个位置的数组,每个位置是一个SyncList,每一个SyncListSyncData的单项链表,每个SyncData中的data存储的是对象的地址。

因此,sDataLists的数据结构可以用下图来表示:

image.png

四、@synchronized 原理分析

id2data 函数分析

进入重点函数id2data,已做注释:

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);// 这个锁是本函数内用的,一定要和外面的做区分
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;

// ------------ 第一阶段 :快速从缓存中查找
#if SUPPORT_DIRECT_THREAD_KEYS 
    // Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    // 通过KVC方式对线程进行获取 线程绑定的data
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    // 如果线程缓存中有data,执行if流程
    if (data) {
        fastCacheOccupied = YES;

        // 如果在线程空间找到的data 属于当前的对象
        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            // 通过KVC获取lockCount,lockCount用来记录 被锁了几次,即 该锁可嵌套
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }

            switch(why) {
            case ACQUIRE: {
                // objc_sync_enter走这里,传入的是ACQUIRE -- 获取
                lockCount++; // 通过lockCount判断被锁了几次,即表示 可重入(递归锁如果可重入,会死锁)
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE:
                //objc_sync_exit走这里,传入的why是RELEASE -- 释放
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {
                    // remove from fast cache
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif

// ------------ 第二阶段 :线程缓存中查找
    // Check per-thread cache of already-owned locks for matching object
    SyncCache *cache = fetch_cache(NO);
    //如果cache中有,方式与线程缓存一致
    if (cache) {
        unsigned int i;
        for (i = 0; i < cache->used; i++) { //遍历总表
            SyncCacheItem *item = &cache->list[i];
            if (item->data->object != object) continue;

            // Found a match.
            result = item->data;
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }
                
            switch(why) {
            case ACQUIRE://加锁
                item->lockCount++;
                break;
            case RELEASE://解锁
                item->lockCount--;
                if (item->lockCount == 0) {
                    // remove from per-thread cache 从cache中清除使用标记
                    cache->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }

    // Thread cache didn't find anything.
    // Walk in-use list looking for matching object
    // Spinlock prevents multiple threads from creating multiple 
    // locks for the same new object.
    // We could keep the nodes in some hash table if we find that there are
    // more than 20 or so distinct locks active, but we don't do that now.
    lockp->lock();
    
// ------------ 第三阶段:如果当前对象存储过,遍历SyncData链表
    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {//cache中已经找到
            if ( p->object == object ) {//如果不等于空,且与object相等
                result = p;//赋值
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);//对threadCount进行++
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object 没有与当前对象关联的SyncData
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // an unused one was found, use it 第一次进来,没有找到
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }

    // Allocate a new SyncData and add to list.
    // XXX allocating memory with a global lock held is bad practice,
    // might be worth releasing the lock, allocating, and searching again.
    // But since we never free these guys we won't be stuck in allocation very often.
// ------------ 第四阶段:创建一个新的 SyncData,头插法插入到链表中
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    // 头插法插入到链表中
    result->nextData = *listp;
    *listp = result;
    
// ------------ 第五阶段 :错误异常报错 崩溃处理,正确存入缓存
 done:
    lockp->unlock();
    if (result) {
        // Only new ACQUIRE should get here.
        // All RELEASE and CHECK and recursive ACQUIRE are 
        // handled by the per-thread caches above.
        if (why == RELEASE) {
            // Probably some thread is incorrectly exiting 
            // while the object is held by another thread.
            return nil;
        }
        if (why != ACQUIRE) _objc_fatal("id2data is buggy");
        if (result->object != object) _objc_fatal("id2data is buggy");

#if SUPPORT_DIRECT_THREAD_KEYS
        if (!fastCacheOccupied) {//判断是否支持栈存缓存,支持则通过KVC形式赋值 存入tls
            // Save in fast thread cache
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
        } else 
#endif
        {
            // Save in thread cache 缓存中存一份
            if (!cache) cache = fetch_cache(YES); //第一次存储时,对线程进行了绑定
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }

    return result;
}
复制代码

TLS 线程相关解释

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

  • pthread_key_create()
  • pthread_getspecific()
  • pthread_setspecific()
  • pthread_key_delete()

总结

  • 数据结构是sDataLists,是一个全局的哈希表,通过拉链法解决哈希冲突,拉链的是syncData类型。
  • 列表存储的是synclistsynclist绑定的是objc对象。
  • @synchronized封装了objc_sync_enterobjc_sync_exit,他们是对称出现,还封装了recursive_mutex_t递归锁。
  • 两种存储 : TLScache
  • 新建的syncData采用头插法,插入链表,标记threadCount = 1
  • 如果是同一个对象进来,TLS -> lockCount ++
  • TLS找不到syncDatathreadCount ++
  • objc_sync_exit进来时,lockCount--threadCount--
  • @Synchronized : 可重入,递归,多线程
    • TLS保障threadCount多少条线程对这个对象加锁。
    • lockCount记录进来多少次。

五、@synchronized 的注意事项

1. @synchronized 为什么锁对象用 self

  • 如果用其他对象,生命周期不能保证,如果被锁对象提前释放,会触发解锁操作,锁内代码不安全。

注:当对象被释放时,调用objc_sync_enterobjc_sync_exit,什么也不会做,这把锁就失去作用了。

2. 为什么 @synchronized 耗时严重

  • 对象被锁后(比如self),为了确保锁内代码安全,我们锁了对象的所有操作。
  • 最直接的影响是,被锁线程变多,执行操作时,查找线程和查找任务都变得很耗时,而且每个被锁线程内的任务还是递归持有,更耗时。

注:我们查询任务时,可能经历3次查询(快速缓存查询->线程缓存查询->遍历所有线程查询),需要寻找线程、匹配被锁对象,nextData递归寻找任务。这些,就是耗时的点。(self需要处理的事务越多,占有的线程数threadCount和每个线程内的锁数量lockCount都会越多,查询也更耗时。)

3. @synchronized 模拟器为什么比真机慢

看一下sDataListsStripeCount

image.png

  • 可以看到真机的时候StripeCount的值是8模拟器时是64,这样模拟器的开销和计算就会多一些,所以更耗时。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享