OC底层原理探索之@synchronized锁

2,141 阅读3分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

各种锁真机性能

在我们的认知里,我们是否觉得@synchronized耗费的性能开销最大,在实测中,发现并非如此。

锁.png

/** OSSpinLock 性能 */
        OSSpinLock kc_spinlock = OS_SPINLOCK_INIT;
        double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < kc_runTimes; i++) {// kc_runTimes =  100000
            OSSpinLockLock(&kc_spinlock);          //解锁
            OSSpinLockUnlock(&kc_spinlock);
        }
        double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
        KCLog(@"OSSpinLock: %f ms",(kc_endTime - kc_beginTime)*1000);
/** ... */

真机验证的情况如下: image.png 模拟器验证的情况如下: image.png​ 所以在真机的时候@synchronized性能不是最差的。我们平时使用的时候使用最广的也是@synchronized

@synchronized原理分析上

  @synchronized (self) { }

这里的参数我们平常传入的都是self,那么传入的self意义到底是什么?我们xcrun一下,或者控制台输入clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m此时就得到了一个.cpp文件,打开搜索直接定位到main函数里

首先直接去掉catch里,重点关注try里面的代码块,格式整理下

id _rethrow = 0;
id _sync_obj = (id)appDelegateClassName;
objc_sync_enter(_sync_obj);
try {
    struct _SYNC_EXIT {
          _SYNC_EXIT(id arg) : sync_exit(arg) {}
          ~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
          id sync_exit;
     }
    _sync_exit(_sync_obj);
}

这里的_SYNC_EXIT是一个结构体,里面有一个构造函数和一个析构函数。这个结构体可以摘出去到方法外面,此时得到了

objc_sync_enter(_sync_obj);
_sync_exit(_sync_obj);

替换掉析构函数得到:

objc_sync_enter(_sync_obj);
objc_sync_exit(_sync_obj);

此时就得到了@synchronized关键的两个函数。我们在LibObjc源码里搜索下 ​

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

从上面的注释可以看到,如果我们参数传递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;
}

从源码也可以看出这两个是成对的函数,一个进去一个出来。从上面可以看出,重点是在SyncData* data = id2data(obj, RELEASE)这一行代码里。点击进去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;

SyncList分析

进入函数id2data,发现SyncData从一个叫做sDataLists的全局静态表里面获取,那么这个表是什么?

spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

StripedMap实际上是一个哈希表, 我们可以在这里断点p一下 image.png 拉链法.png

当obj对象相同的时候,使用拉链法存储SyncData,当加锁的时候就加进去,因为不需要查询,所以使用这样的数据存储结构最巧妙。 ​

@synchronized原理分析下

id2data分析把条件判断先折叠起来

#if SUPPORT_DIRECT_THREAD_KEYS
#endif

这里面的逻辑是一样的,如果有tls就查找tls如果没有就查找Cache缓存。第一次进来的时候是没有data的。首先进来的是这行代码

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; 

最后两句说明这个链表是头插法。先创建,之后再存储到表里。下次进来的时候,data有值

 SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data->object == object)

如果是同一个对象则判断当前的对象锁了多少次,lockCount对应变更。如果当前的lockCount=0表明当前已经解锁完成了,就从当前的缓存中删除;如果不是同一个对象则重新创建一个SyncList

总结:@synchronized最重要的就是两个标识。threadCount来标记当前的对象被多少条线程加锁过,lockCount表明在同一个线程该对象被锁过多少次。这两个标识表明了@synchronized具有多线程可递归性。 ​

@synchronized总结

  1. SyncList是一张哈希表,采取的是拉链法存储syncData
  2. sDatalistarray存储的是synclist(objc)
  3. 底层的函数是objc_sync_enter/exit对称递归锁
  4. 采取两种存储:tls/cache
  5. 第⼀次没有syncData时,通过头插法链表创建, 标记threadcount=1
  6. 第二次进入判断是不是同⼀个对象,不是重新创建标记
  7. 是的话,如果TLS找得到->lock++
  8. TLS找不到重新创建一个syncData并且对threadCount++
  9. 如果是exit函数 lock-- threadCount--

Synchronized:可重⼊ 递归 多线程 锁的对象不要为空 ​

我们平时加self的原因一个是生命周期的管理保证加锁的对象不会意外被释放掉,另一个原因是我们知道syncList的值是objc,在同一个页面使用一个self,只对当前的self拉链,方便存储和释放。另外开头的实验我们发现模拟器和真机的synchronized性能差异比较大

template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif

如果是真机的话count = 8表明可以有8个线程对比模拟器的话,查的比较快。