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_enter
和objc_sync_exit
都是在libobjc.A.dylib库中
,此时可以打开objc的源码一看究竟。
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
,可以大概看出如下关系结构图:
函数中从上至下可以拆分成五个板块:
板块一:
查找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
经过这个板块进行了加一操作,证明了其实支持多线程的:
因为在多线程和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
,至于出现哈希冲突,会进行再哈希。
总结
-
sDataLists
是一张全局的StripedMap
类型哈希表,采用拉链法存储syncData
; -
aDataLists
是一个数组,但是其存储数据并不是按循序存,是进行了哈希,如果出现哈希冲突,然后再哈希; -
objc_sync_enter
/objc_sync_exit
是成对出现的,其封装了递归锁,都会走到id2data函数
- 采用两个存储方式:
TLS
和cache
,这两种可能同时使用,也可能只用一种 -
syncData
采用头插法存到链表中,并标记threadCount = 1
- TLS获取的
data
(同一线程中)如果是同一个对象,则进行lockCount++
/lockCount--
操作 - 如果TLS没有data,找不到,则
syncData
的threadCount++
操作