OC底层探索24-synchronize锁的原理OC底层探索24-synchronize锁的原理

2021-08-09 11:09:15 浏览数 (1)

1、八大锁效率

  • 八大锁分别:
    • 自璇所:OSSpinLock。在iOS10以后该锁被重写,会在堵塞时进行休眠
    • 互斥锁:NSLock、NScondition、NSRecursiceLock、NSConditionLock、@synchronize;以及更加偏底层:pthread_mutex、pthread_mutex(recursive);

2、synchronize探索入口

所有底层的探索都需要一个切入点,像这样的代码段除了堆栈的方式,还有clang、查看汇编的方式。

代码语言:javascript复制
@synchronized (self) {
    i  = 1;
}
2.1 查看堆栈

事实证明在这个问题上是不适用的;

2.2 汇编方式
  • 可以看到使用了@synchronize之后在方法块前后调用了两个方法objc_sync_enterobjc_sync_exit;

继续增加objc_sync_enter的符号断点之后;

  • @synchronize是属于libobjc.A.dylib库的;
  • objc_sync_enter在底层callq(调用)函数id2data(objc_object*, usage);
2.3 clang方式

使用命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o vc.cpp

  • 根据clang获取编译后的代码,也可以看到熟悉的两个方法objc_sync_enterobjc_sync_exit,同时也验证了汇编方式的结论;

3、objc_sync_enter 源码分析

通过符号断点,得知@synchronize是在我们熟悉的libobjc库中,在我之前的文章中可以得到OC底层探索02- objc4-781 源码编译;

代码语言:javascript复制
enum usage { ACQUIRE, RELEASE, CHECK };

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        // ACQUIRE 枚举值
        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;
}

BREAKPOINT_FUNCTION(
    //其实什么都没有做
    void objc_sync_nil(void)
);

看到这段代码之后再回头看看objc_sync_enter的汇编部分,是不是发现其实汇编也就那样;

  • data->mutex.lock()这才是真正的加锁操作,是系统recursive_mutex_t递归互斥锁的更高层封装;
  • 如果传入的obj是个空值,系统是没有做任何事的,所以在使用时要保证标示对象一定不能为空
  • 通过异常判断之后进入函数id2data(obj, ACQUIRE);

4、objc_sync_exit

代码语言:javascript复制
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;
}
  • 同样也是进入到了id2data(obj, RELEASE)只是第二个参数不一样。提现了无处不在的抽象和封装思想;

5、id2data(obj, enum usage) 核心函数

代码非常长,这里分为四步分来分析

代码语言:javascript复制
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;
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
        // 第一部分
    }
#endif

    // Check per-thread cache of already-owned locks for matching object
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        // 第二部分
    }

    lockp->lock();


    //第三部分
    
 done:
    lockp->unlock();
    if (result) {
        // 第四部分
    }

    return result;
}
3.3.1 第一部分 快速缓存
代码语言:javascript复制
#if SUPPORT_DIRECT_THREAD_KEYS
// Check per-thread single-entry fast cache for matching object
// 检查当前线程的快速缓存
bool fastCacheOccupied = NO;
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
    // 标示快速缓存被占用;防止后续该线程的其他锁进行替换,而导致的问题;
    fastCacheOccupied = YES;
    // 快速缓存中找到该缓存对象
    if (data->object == object) {
        // Found a match in fast cache.
        uintptr_t lockCount;
        // lockCount标记该锁的加锁次数
        result = data;
        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: {
            lockCount  ;
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
            break;
        }
        case RELEASE:
            lockCount--;
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
            if (lockCount == 0) {
                // remove from fast cache
                // 缓存次数为0后,将快速缓存对象制空
                tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                // atomic because may collide with concurrent ACQUIRE
                // 原子性的对缓存对象的线程使用数减一
                OSAtomicDecrement32Barrier(&result->threadCount);
            }
            break;
        }
        //找到处理完lockCount后直接返回
        return result;
    }
}
#endif
  • 在没有特别设置:SUPPORT_DIRECT_THREAD_KEYS默认为1;
  • 当前缓存的快速缓存: 当前线程第一次加锁的对象会被定义为快速缓存;(大多数情况下,一条线程只会使用一个标示对象进行加锁);
  • SYNC_DATA_DIRECT_KEYSYNC_COUNT_DIRECT_KEY都是在当前线程的局部缓存中查找缓存对象SyncData缓存次数lockCount
3.3.1 SyncData

在快速缓存阶段,系统保存了结构为SyncData的对象。

代码语言:javascript复制
typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // 使用该锁的线程数
    recursive_mutex_t mutex;    // 递归互斥锁
} SyncData;
  • SyncData锁对象对象,是一个链表结构;
  • SyncData将synchronize锁所需要的数据进行保存;
3.3.2 第二部分 慢速缓存

这一部分涉及到了慢速缓存,如果在快速缓存中没有找到则会来到这部分;

代码语言:javascript复制
// Check per-thread cache of already-owned locks for matching object
SyncCache *cache = fetch_cache(NO);
if (cache) {
    unsigned int i;
    for (i = 0; i < cache->used; i  ) {
        SyncCacheItem *item = &cache->list[i];
        if (item->data->object != object) continue;

        // 这部分和快速缓存操作基本一致
        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) {
                // 缓存数组的总个数减少
                cache->list[i] = cache->list[--cache->used];
                // 原子性操作
                OSAtomicDecrement32Barrier(&result->threadCount);
            }
            break;
        }
        return result;
    }
}
  • 测试后发现,慢速缓存也是从当前线程的进行查找
  • cache->listi = cache->list--cache->used;将数组最后一个对象移动到当前下标位置,然后将数组进行缩容;
  • 通过这个双重缓存结构,提高了锁对象syncdata的查找效率;
3.3.2 SyncCache

在慢速缓存中出现了这样一个结构SyncCache.

代码语言:javascript复制
typedef struct {
    SyncData *data; // 锁对象
    unsigned int lockCount;  // 缓存次数
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;  // 缓存数组的个数
    SyncCacheItem list[0];  // 锁对象的列表
} SyncCache;
  • SyncCache是慢速缓存的实体体现;
  • SyncCacheItem包含了SyncData锁对象以及该锁对象的缓存次数;
3.3.3 第三部分

在双重缓存下都没有命中后会来到这部分,这部分会在:初次加锁同一对象不同线程加锁的时候进入.

代码语言:javascript复制
lockp->lock();
    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        //  listp在函数最开始进行的获取
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {
                // 同一对象不同线程加锁进入这里
                result = p;
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
        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;
        }
    }
    // 全新创建
    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;
  1. 同一对象不同线程加锁时会进行原子的threadCount
  2. 由于list中SyncData不会进行删除,所以需要复用
  3. 如果1、2步都没有名字,则进行全新创建,并保存到节点的第一个;
3.3.3 StripedMap

listp是在函数最开始进行获取,锁对象存储结构。通过对object的地址hash计算后确定数组下标;

代码语言:javascript复制
SyncData **listp = &LIST_FOR_OBJ(object);

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

class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];
    //哈希算法
    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }
}
  • StripedMap在OC底层探索19-weak和assign区别浅谈在分析weak存储结构时也出现过,都是通过hash算法来进行分组,减少数据查找的难度;
  • 不同的是weak的StripedMap对应的是一张SideTable表;而@synchronized的StripedMap对应的是一个链表结构;

synchronized结构

3.3.4 第四部分 done
代码语言:javascript复制
done:
    lockp->unlock();
    if (result) {
        // 解锁流程
        if (why == RELEASE) {
            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) {
            // Save in fast thread cache
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
        } else 
#endif
        // 否则保存到当前线程的慢速缓存list中
        {
            // 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;
  • 在第三部分处理完之后都会来到done中;
  • 快速缓存和慢速缓存会互斥存在

总结

通过函数id2data的参数完成了加、解锁操作。并且使用了快速缓存、慢速缓存双重缓存,来提高synvData的命中速度。除此之外stiped syncData链表对锁实体进行保存。利用threadCount lockCount实现了多线程、重复加、解锁操作;

通过这些操作提高了递归锁的安全性,但是也降低了性能;

补充

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

还有的几种锁,以后有机会在探索吧~毕竟大部分都在Founation库中,不是很好分析。

欢迎在留言区和我沟通!

0 人点赞