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

  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 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 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线程锁)