美文网首页
iOS底层探索--@synchronized线程锁

iOS底层探索--@synchronized线程锁

作者: spyn_n | 来源:发表于2021-12-08 12:11 被阅读0次

      iOS中各种锁性能对比,建立一个10万次的循环,加锁、解锁,对比前后时间差得到其耗时时间。以下是真实的测试结果,不一样的架构以及不一样的iOS系统,运行结果存在一定的差异,所以对比差异没有多大的实际意义,但是同一环境,不一样的锁的耗时差异是值得参考的。

    输出对比:

    模拟器 iPhone12 mini( iOS14.5) 真机iPhone 6 (iOS 12.5.3)

    图标对比:

    各个锁性能对比

    通常在开发中@synchronize使用最为简单,功能也相对强大,不足的就是耗时最长。

    @synchronized锁探索

    1、测试代码

    下面以最简单、干净的测试代码开启@synchronize的底层研究:

    int main(int argc, char * argv[]) {
        NSObject *objc = [[NSObject alloc] init];
        @synchronized (objc) {
        }
        return 0;
    }
    
    2、clang

    clang生成cpp文件
    导出模拟器架构的命令:xcrun -sdk iphonesimulator clang -rewrite-objc main.m

    提炼核心
    撇开结构体定义和失败的部分,重要的是两句代码:
    objc_sync_enter(_sync_obj);
    _sync_exit(_sync_obj);
    根据结构体中析构函数可得:
    objc_sync_enter(_sync_obj); 一个进入
    objc_sync_exit(_sync_obj); 一个退出
    3、符号断点

    新建一个工程,下符号断点,发现objc_sync_enterobjc_sync_exit都是在libobjc.A.dylib库中,此时可以打开objc的源码一看究竟。

    objc_sync_enter符号断点 objc_sync_exit符号断点
    4、源码静态分析(objc-818.2源码

    对比这两个函数如果objc不存在,也就是nil,则do nothing,什么也不做。对于我们而言,有用的流程只有两个,一个是data->mutex.tryLock(),一个是data->mutex.tryUnlock(),简单来讲就是一个为了加锁,一个解锁。最为关键的是,这个data,也就是id2data(obj, ACQUIRE)这个函数返回的数据类型和结构,以及其内部做了什么事情,是我们研究的重点。

    objc_sync_enter objc_sync_exit

    首先看一下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的数据结构可以大概知道,其封装了一个
    recursive_mutex_t --- 递归锁
    threadCount --- 线程数量
    object --- 对象,也就是要给哪个对象加锁
    nextData --- 拉链表下一个数据指针

    id2data函数

    如果要研究一个函数,尤其是底层难懂的代码,首先将花括号折叠,大致看一下流程(如果一头插入细节,很容易迷失自己想要干什么,也很容易放弃)。
    首先了解一下TLS线程:
    线程局部存储(Thread Local Storage,TLS):是操作系统为线程单独提供的私有空间,通常只有有限的容量。Linux系统下通常通过平thread库中的相关函数操作:
    pthread_key_creat()
    pthread_getspecific()
    pthread_setspecific()
    pthread_key_delete()
    由于SUPPORT_DIRECT_THREAD_KEYS = 1,SYNC_DATA_DIRECT_KEY定义如下:

    • define SYNC_DATA_DIRECT_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY1)

    • define __PTK_FRAMEWORK_OBJC_KEY1 41

    // 使用多个并行列表来减少不相关对象之间的争用。
    #define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
    #define LIST_FOR_OBJ(obj) sDataLists[obj].data
    static StripedMap<SyncList> sDataLists;
    

    可以知道存在一张全局静态的哈希表sDataLists,类型是StripedMap,包装的是SyncList,可以大概看出如下关系结构图:

    结构关系图 id2data函数

    函数中从上至下可以拆分成五个板块:

    板块一: 查找TLS(TLS具有线程保证功能)是否有data,如果是相同的对象,则进行lockCount++或者lockCount--操作,记录同一条线程中锁的次数

    板块一

    板块二: 查找Cache缓存,遍历缓存列表cache->list,查看data中对象是否是同一个,从而进行lockCount++或者lockCount--操作,记录同一条线程中锁的次数

    板块二

    板块三:遍历使用链表(拉链法)中相同的对象,则对threadCount进行Increment加1操作。记录同一对象被多少条线程锁住。

    板块三

    板块四: 创建一个新的SyncData,并采用拉链法(头插法)加入list中,记录threadCount=1,封装递归锁recursive_mutex_t

    板块四

    板块五:根据是否支持TLS存储选择TLS或者Cache存储(相比之下TLS比Cache更加高效)

    板块五

    通过上面静态分析源码,发现synchronized锁具有可重入,可递归,支持多线程的一把锁。

    5、LLDB动态调试

      下面进行LLDB动态调试,看看其创建结构图的流程是怎么走的。同时为了研究方便,直接在main函数写代码,在第一个@synchronized (p1)下断点,然后再在函数id2data的五个板块下断点,进行单步调式。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            NSObject *p1 = [NSObject alloc];
            NSObject *p2 = [NSObject alloc];
            NSObject *p3 = [NSObject alloc];
            for (int i = 0; i < 10; i++) {
                dispatch_async(dispatch_get_global_queue(0, 0), ^{
                    @synchronized (p1) {
                        NSLog(@"p1第1个@synchronized");
                        @synchronized (p2) {
                            NSLog(@"p2第二个@synchronized");
                            @synchronized (p1) {
                                NSLog(@"p1第二个@synchronized");
                                @synchronized (p3) {
                                    NSLog(@"p3第1个@synchronized");
                                }
                            }
                        }
                    }
                });
            }
        }
        return 0;
    }
    

      这里说明一点,由于多线程的影响,所以断点单步调试可能并不像自己所预想的那样走,会导处乱串,所以需要耐性,也可以线从单线程(主线程)开始研究。(有时候靠点运气)


    线程乱串证据
    class StripedMap {
    #if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
        enum { StripeCount = 8 };  // 真机下这个表大小8
    #else
        enum { StripeCount = 64 }; // 模拟器下这个表大小64
    #endif
    }
    

    通过调试,发现板块四首先走,当板块四走完之后,打印那张全局的StripedMap确实有64个,且经过艰难的查找发现在下标1处找到了刚刚创建的SyncData,因为是经过哈希得到的下标,所以不一定是从0开始:

    64个 image.png

    笔者在调试的时候发现在板块五存储的时候是走的TLS存储。


    TLS存储

    在进入板块三for循环的时候,对于同一个对象,其threadCount经过这个板块进行了加一操作,证明了其实支持多线程的:

    threadCount+1

    因为在多线程和64这么大的表中,形成拉链的概率小,并且断点有时候都断不住,既然上面已经证明了支持多线程,那么我干脆直接在主线程中研究,同时为了增加哈希冲突的概率,笔者直接将StripeCount = 64改成StripeCount = 2

    class StripedMap {
    #if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
        enum { StripeCount = 8 };  // 真机下这个表大小8
    #else
        enum { StripeCount = 2 };  // 2
    #endif
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSObject *p1 = [NSObject alloc];
            NSObject *p2 = [NSObject alloc];
            NSObject *p3 = [NSObject alloc];
            for (int i = 0; i < 5; i++) {
                    @synchronized (p1) {
                        NSLog(@"p1第1个@synchronized");
                        @synchronized (p2) {
                                @synchronized (p3) {
                                }
                        }
                    }
            }
        }
        return 0;
    }
    
    第一个SyncData

    经过多次运行调试发现,@synchronized (p1),每一个对象都会生成一个syncData,至于出现哈希冲突,会进行再哈希。

    拉链形成 证明头插法
    总结
    1. sDataLists是一张全局的StripedMap类型哈希表,采用拉链法存储syncData
    2. aDataLists是一个数组,但是其存储数据并不是按循序存,是进行了哈希,如果出现哈希冲突,然后再哈希;
    3. objc_sync_enter / objc_sync_exit是成对出现的,其封装了递归锁,都会走到id2data函数
    4. 采用两个存储方式:TLScache,这两种可能同时使用,也可能只用一种
    5. syncData采用头插法存到链表中,并标记threadCount = 1
    6. TLS获取的data(同一线程中)如果是同一个对象,则进行lockCount++ / lockCount--操作
    7. 如果TLS没有data,找不到,则syncDatathreadCount++操作
    表的关系结构图

    相关文章

      网友评论

          本文标题:iOS底层探索--@synchronized线程锁

          本文链接:https://www.haomeiwen.com/subject/ajkabltx.html