前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OC底层探索24-synchronize锁的原理OC底层探索24-synchronize锁的原理

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

作者头像
用户8893176
发布2021-08-09 11:09:15
6290
发布2021-08-09 11:09:15
举报
文章被收录于专栏:小黑娃Henry

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库中,不是很好分析。

欢迎在留言区和我沟通!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/7/6 下午,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、八大锁效率
  • 2、synchronize探索入口
    • 2.1 查看堆栈
      • 2.2 汇编方式
        • 2.3 clang方式
        • 3、objc_sync_enter 源码分析
        • 4、objc_sync_exit
        • 5、id2data(obj, enum usage) 核心函数
          • 3.3.1 第一部分 快速缓存
            • 3.3.1 SyncData
          • 3.3.2 第二部分 慢速缓存
            • 3.3.2 SyncCache
          • 3.3.3 第三部分
            • 3.3.3 StripedMap
          • 3.3.4 第四部分 done
            • 补充
        • 总结
        相关产品与服务
        对象存储
        对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档